3 While playing ASIS CTF Finals 2019, I worked on the following challenges.
7 | Points | Solves | Category |
8 |--------|--------|----------|
13 > We have built an area protected by a hard password
15 > Note: DO NOT Brute force the server (the rate limit will ban you), the question may need an OFFLINE brute-force!
17 This challenge presents us with a webpage, that displays some text and after an instance, some more text apears. Therefore, we immediately look at the source code and find the resposible javascript in `app.js`:
20 var file_check = function(file){
23 url: '/check_perm/readable/',
25 }).done(function(data){
34 var file_read = function(file){
39 }).done(function(data){
46 var update_page = function(text){
50 $(document).ready(function() {
51 console.log("ready!");
53 file_check('public.txt');
57 So there apear to exist at least two endpoint, `/check_perm/readable/` and `/read_file/`. We look at the first one and notice that it returns `True` supossedly if the requested file exists and we can read it, otherwise `0` is returned. The way of writing `True` with capital `T` strongly suggestets that we are dealing with a python backend, probably flask.
59 The other endpoint returns the content of the requested file. Just out of curiosity we try `http://66.172.33.148:8008/read_file/?file=private.txt` but no flag there. Requests for files like `flag.txt` returns `500`, while `flag` without ending returns `Security`. Strange. Further investigation shows that all non-txt file requests return `500` (probably file does not exists) while requests for other file endings return `Security`. Therefore we supose, only txt files can be returned.
61 A common vulnerability of file access systems like this is path traversal, where we use a relative path to break out of the current directory and access files outside of the normal scope. We can try this by requesting `../../../../../../../../../../etc/passwd` for both endpoints. The first one does return `True`, so we know that path traversal is possible, however no luck on the second one, that returns `Security` as expected, since it's no txt file. Adding a `.txt` ending to the file turns this into a `500` response, since that file does not exist.
63 My guess now was that we need to exploit some weird behaviour of the file open funtion in python, maybe including wildcards like `*` or `?`, but no luck there. This would only work if the app uses the `glob` function. However we notice something interesting, namely the second endpoint does strip out `../`, while the first one does not. We can gain path traversal on the second endpoint with `..././`. I also tried to exploit this fact to bypass the txt-only check with requests for files like `/etc/passwd.tx../t` but this does not help in any way. This is where everything got stuck a bit and I continued with mapping out what files are on the system and trying to find out where our current working directory is but no jucy information was to be found.
65 We came to a solution when someone in the mattermost chat found the trick to include non-txt files, like so `http://66.172.33.148:8008/read_file/?file=....//....//....///app/main.py&test=ha.txt`. This means the app is checking the whole argument string for a txt ending and not only the relevant paramter. With this, we where able to download the app code, which, apart from the stuff we already figured out, had some intersting functionality included:
68 from .functions import *
70 @app.route('/protected_area_0098', methods=['GET'])
72 def app_protected_area() -> str:
76 and looking at `functions.py`, we see:
81 Wraps routing functions that require a user to be logged in
84 def wrapper(*args, **kwds):
86 ah = request.headers.get('ah')
88 if ah == hashlib.md5((Config.ADMIN_PASS + Config.SECRET).encode("utf-8")).hexdigest():
89 return f(*args, **kwds)
97 So we finaly check `http://66.172.33.148:8008/read_file/?file=....//....//....///app/config.py&test=ha.txt`:
100 FLAG = os.environ.get('FLAG')
102 ADMIN_PASS = "b5ec168843f71c6f6c30808c78b9f55d"
105 So we can craft the request as follows `curl -H "ah: cbd54a3499ba0f4b221218af1958e281" http://66.172.33.148:8008/protected_area_0098` and jackpot: `ASIS{f70a0203d638a0c90a490ad46a94e394}`
109 Download pictures, exiftool, binwalk, foremost, strings, get comments, get base64 string, get password, extract palete, test for stegano, find DOS executale, try making it work, more stegano, even more, find out about lsb in certain images, extract bits, try figuring it out manually, notice commend makes kind of sense, use tools, different outputs, look into password algos
111 https://github.com/apsdehal/awesome-ctf#forensics-1
112 https://aperisolve.fr/
113 https://github.com/DominicBreuker/stego-toolkit
114 https://georgeom.net/StegOnline/extract
115 https://github.com/krx/CTF-Writeups/blob/master/CSAW%2016%20Quals/for250%20-%20Watchword/jk_actual_writeup.md
116 https://hexpresso.wordpress.com/2014/05/10/asis-ctf-quals-2014-stego-100-blocks-write-up/
117 https://honeysec.blogspot.com/2019/05/writeup-csactf19-challenges.html?m=1
118 https://shankaraman.wordpress.com/category/ctf/stegano/
119 https://veteransec.com/2018/10/18/vetsec-takes-first-in-the-hacktober-ctf-summary-steganography-write-up/
124 This crypto challenge presents us with some code used to encrypt a flag, we also get the output of said encryption operation:
127 #!/usr/bin/env python
129 from Crypto.Util.number import *
130 from flag import flag
136 S += float(a)/float(l)
140 s, a = S, float(a)/float(l)
145 if p % 9 == 1 and p % 27 >= 2:
146 q = gmpy2.next_prime(serifin(p, 3) + serifin(p, 9) + serifin(p, 27))
147 if q % 9 == 1 and q % 27 >= 2:
148 return int(p), int(q)
156 p, q = genPrime(nbit)
164 c = 78643169701772559588799235367819734778096402374604527417084323620408059019575192358078539818358733737255857476385895538384775148891045101302925145675409962992412316886938945993724412615232830803246511441681246452297825709122570818987869680882524715843237380910432586361889181947636507663665579725822511143923
165 n = 420908150499931060459278096327098138187098413066337803068086719915371572799398579907099206882673150969295710355168269114763450250269978036896492091647087033643409285987088104286084134380067603342891743645230429893458468679597440933612118398950431574177624142313058494887351382310900902645184808573011083971351
168 So at a first glance, this looks like textbook-RSA with `e = 3`. What is even more suspicious is that the primes get derived with some weird, fishy looking algorithm. Without further analysing that I checked factordb if the `n` has known factors and indeed, we get lucky:
172 10718841513477905819120058147135847238291365362593720675636253771082993743788743083816117407101738585993985506647694375447285228211151896705183442597773779L
174 39268063621491165558293370755773815300739539018598511154583095766289789691669875090357609442626770422364550257841199885538175542665396553281747272813511469L
177 So this should be easy from there, just do regular RSA decryption and we are done here.
179 But wait, not so fast. As it turns out, `gcd(3, phi(n)) = 3`. Doh! So RSA decryption does no work since there is no multiplicative inverse `e^-1 mod n`. So I searched the web for this problem of RSA and found two relevant CTF challenges that look similar here https://github.com/p4-team/ctf/tree/master/2016-03-12-0ctf/rsa and here https://github.com/p4-team/ctf/tree/master/2015-10-18-hitcon/crypto_314_rsabin#eng-version
181 The first link looks especially promising so I tried to implement their code used to solve it. On of the main problems is finding the cube root `mod n` boils down to find the cube root `mod p` or `mod q` accordingly, thanks to the Chinese Remainder Theorem. Unfortunately I had less luck than the authors of the write up, since Wolfram Alpha was not able to calculate the roots for me. So I ended up using this code I found on Stackoverflow:
184 #assumes p prime, it returns all cube roots of a mod p
187 #Non-trivial solution of x^r=1
190 while pow(t,(p-1)/r,p)==1: t-=1
191 return pow(t,(p-1)/r,p)
193 def solution(p,root):
195 return [root%p,(root*g)%p,(root*g^2)%p]
200 if p in [2,3] or a==0: return [a]
201 if p%3 == 2: return [pow(a,(2*p - 1)/3, p)]
203 #There are 3 or no solutions
206 if pow(a,(p-1)/3,p)>1: return []
209 if p%9 == 4: #[13, 31, 67]
210 root = pow(a,(2*p + 1)/9, p)
211 if pow(root,3,p) == a: return solution(p,root)
214 if p%9 == 7: #[7, 43, 61, 79, 97
215 root = pow(a,(p + 2)/9, p)
216 if pow(root,3,p) == a: return solution(p,root)
219 if p%27 == 10: #[37, 199]
220 root = pow(a,(2*p +7)/27, p)
223 if pow(root,3,p) == a: return solution(p,root)
227 if p%27 == 19: #[19, 73, 127, 181]
228 root = pow(a,(p + 8)/27, p)
231 if pow(root,3,p) == a: return solution(p,root)
234 #We need an algorithm for the remaining cases
235 return tonelli3(a,p,True)
238 Here I noticed some similarities with the original code, namely the line `if q % 9 == 1 and q % 27 >= 2`. This looks like we are on the right track and indeed, `p` and `q` are equal to `10`, `19` `mod 27` respectively. Si we know we will find the roots if the exist and indeed we find 3 roots for `p` but none for `q` so this leads is also a dead end. I looked into the other link, namely Rabin's Algorithm, where we can decrypt a similar text if `e` is a power of 2, but I could no come up with a way to get rid of the factor 3, which remains always regardles of the fator we mutliply it with. I also read more about CRT and found some more interesting links here http://www.math.clemson.edu/~sgao/crypto_mod/node3.html and here https://www.rootnetsec.com/picoctf-weird-rsa but nothing led anywhere so this is how far I came during the CTF.
240 Afterwards, this write-up got published https://github.com/p4-team/ctf/tree/master/2019-11-16-asis-finals/serifin
242 So aparently I was not that far off, I just made a mistack at calculating the roots, somehow the algorithm I used faild me. What we learn is, never blindly trust code. Also my understanding of the matter was not profound enough to really reflect on what is going on, I mostly just mimicked what other people where doing, still lot's to learn.
246 look at code, discuss with collegues, propose decryption,
250 same pow as before, check if there are scripts for next prime, note mistake in condition, naive gmpy next_prime implementation
252 https://math.stackexchange.com/questions/35300/what-is-the-most-efficient-algorithm-to-find-the-closest-prime-less-than-a-given
253 https://stackoverflow.com/questions/10143431/finding-the-nth-twin-prime