loading

【笔记】pwn.college之Playing With Programs(pwn.college)

Dealing with Data 数据处理

What's the password? 密码是多少?

让我们从简单的开始,开启编码之旅。这个程序需要一个密码,但你无法知道它是什么……除非你阅读它!

在大多数网络安全分析环境中,您将分析的不是您编写的软件,就像这个程序。因此,您在本模块中学到的第一个技能就是阅读软件,以了解它希望您发送的数据。我们将从这个简单的 Python 程序开始。

该程序位于 /challenge/runme,在给您 flag 之前会要求一个复杂的密码。这将是您学习过程中阅读的最简单的程序,因为它只是从标准输入读取数据并进行一次简单的检查。

阅读该程序,理解 Python 代码,让程序给您 flag!

查看解析
cat /challenge/runme
#!/usr/bin/exec-suid -- /bin/python3 -I
import sys
print("Enter the password:")
entered_password = sys.stdin.buffer.read1().strip()
correct_password = b"uzpcquax"
print(f"Read {len(entered_password)} bytes.")
if entered_password == correct_password:
    print("Congrats! Here is your flag:")
    print(open("/flag").read().strip())
else:
    print("Incorrect!")
    sys.exit(1)
可知密码为uzpcquax

... and again! ……再来一次!

亲爱的黑客,再次投入战斗!只是为了确保您理解了思路。

查看解析
cat /challenge/runme
#!/usr/bin/exec-suid -- /bin/python3 -I
import sys
print("Enter the password:")
entered_password = sys.stdin.buffer.read1().strip()
correct_password = b"htwtyeml"
print(f"Read {len(entered_password)} bytes.")
if entered_password == correct_password:
    print("Congrats! Here is your flag:")
    print(open("/flag").read().strip())
else:
    print("Incorrect!")
    sys.exit(1)
可知密码为htwtyeml

Newline Troubles 换行符的困扰

之前的挑战相当简单,这个也是。但它做了一件略有不同的事情:它不会忽略您在终端输入密码时按下的 Enter 键。这导致您的 entered_password 包含一个换行符,而由于 correct_password 没有换行符,比较失败!

这类问题——数据中的多余分隔符——总是发生,并且可能导致大量时间浪费。在这个关卡中,有几种方法可以解决它:

  1. 研究如何按 Enter 键来终止终端输入。这非常容易在线搜索到!
  2. 回忆一下在 Linux Luminarium 中,如何将 echo(带有禁用换行符的参数)重定向到 /challenge/runme 的标准输入。
  3. 创建一个没有换行符的文件,并记住您在 Linux Luminarium 中学到的知识,将文件重定向到 /challenge/runme 的标准输入。

祝你好运!

查看解析
cat /challenge/runme
#!/usr/bin/exec-suid -- /bin/python3 -I
import sys
print("Enter the password:")
entered_password = sys.stdin.buffer.read1()
if b"\n" in entered_password:
    print("Password has newlines /")
    print("Editors add them sometimes /")
    print("Learn to remove them.")
correct_password = b"gszudhjc"
print(f"Read {len(entered_password)} bytes.")
if entered_password == correct_password:
    print("Congrats! Here is your flag:")
    print(open("/flag").read().strip())
else:
    print("Incorrect!")
    sys.exit(1)
可知密码为gszudhjc,但是不能包含换行符
printf 'gszudhjc' | /challenge/runme或者echo -n 'gszudhjc' | /challenge/runme

Reasoning about files 关于文件的推理

让我们探索程序可能获取安全相关输入的其他方式。这里,程序从终端读取密码。您还能破解它吗?

查看解析
cat /challenge/runme
#!/usr/bin/exec-suid -- /bin/python3 -I
import sys
try:
    entered_password = open("iqvn", "rb").read()
except FileNotFoundError:
    print("Input file not found...")
    sys.exit(1)
if b"\n" in entered_password:
    print("Password has newlines /")
    print("Editors add them sometimes /")
    print("Learn to remove them.")
correct_password = b"xgfkvmlb"
print(f"Read {len(entered_password)} bytes.")
if entered_password == correct_password:
    print("Congrats! Here is your flag:")
    print(open("/flag").read().strip())
else:
    print("Incorrect!")
    sys.exit(1)
可知密码为ekavbpxd,从命令行参数指定的文件中读取密码
echo -n 'ekavbpxd' > iqvn
/challenge/runme

Specifying Filenames 指定文件名

这里还有一个小的变化。您还能得到它吗?

查看解析
cat /challenge/runme
#!/usr/bin/exec-suid -- /bin/python3 -I
import sys
try:
    entered_password = open(sys.argv[1], "rb").read()
except FileNotFoundError:
    print("Input file not found...")
    sys.exit(1)
if b"\n" in entered_password:
    print("Password has newlines /")
    print("Editors add them sometimes /")
    print("Learn to remove them.")
correct_password = b"ekavbpxd"
print(f"Read {len(entered_password)} bytes.")
if entered_password == correct_password:
    print("Congrats! Here is your flag:")
    print(open("/flag").read().strip())
else:
    print("Incorrect!")
    sys.exit(1)
可知密码为ekavbpxd,从命令行参数指定的文件中读取密码
echo -n 'ekavbpxd' > iqvn
/challenge/runme iqvn

Binary and Hex Encoding 二进制和十六进制编码

现在,生活必须变得复杂。您可能已经注意到本模块中密码常量前面的 b 字母。Python 有两种类似字符串的常量:str 字符串(指定为 "asdf")和 bytes 字节(指定为 b"asdf")。让我们在这一关中讨论 bytes

字节是实际存储在计算机内存中的内容。您可能知道,计算机用二进制思考:只是一堆 1 和 0。由于历史原因,我们将这些 1 和 0("位")以 8 个为一组表示,每组 8 个(一个"字节")。这个数字完全是任意的:早期的计算机(大约 1960 年代以前)根本没有这种分组,或者有其他任意的分组。在一个字节是 16、32 或任意位数(尽管出于数学原因,它很可能保持为 2 的幂)的平行宇宙中,这是非常可行的。

单个二进制数字(位)可以表示两个值(01),两个位可以表示四个值(00011011),三个位可以表示八个值(000001010011100101110111),四个位可以表示十六个值。相比之下,单个十进制数字可以表示 10 个值(从 09)。十个值大约由 log2(10) == 3.3219... 位表示,您会遇到一些奇怪的情况,比如二进制 1001 是十进制 9,但二进制 1100(仍然是 4 个二进制数字)是 12两个十进制数字!)。表示十进制和二进制之间这种数字不同步的另一种方式是,十进制没有清晰的位边界

位边界的缺乏使得推理十进制和二进制之间的关系变得复杂。例如,通常很难在十进制和二进制之间进行逐位转换:我们可以算出 97110001,但很难一眼看出。

在数字之间对齐度更高的进制之间进行逐位转换要容易得多。例如,单个十六进制(基数为 16)数字可以表示 16 个值(0123456789abcdef):与二进制在 4 个数字中可以表示的值数量相同!这使我们能够有一个超级简单的映射:

十六进制 二进制 十进制
0 0000 0
1 0001 1
2 0010 2
3 0011 3
4 0100 4
5 0101 5
6 0110 6
7 0111 7
8 1000 8
9 1001 9
a 1010 10
b 1011 11
c 1100 12
d 1101 13
e 1110 14
f 1111 15

这种从十六进制数字到 4 位的映射很容易记忆(最重要的是:记住 1248,您可以快速推导出其余的)。更好的是,两个十六进制数字就是 8 位,也就是一个字节!与十进制不同,对于 4 位您必须记住 16 个映射,对于 8 位必须记住 256 个映射,而对于十六进制,对于 4 位您只需要记住 16 个映射,对于 8 位也只需要记住相同数量的映射,因为它只是两个十六进制数字的连接!一些例子:

十六进制 二进制 十进制
00 0000 0000 0
0e 0000 1110 14
3e 0011 1110 62
e3 1110 0011 227
ee 1110 1110 238

现在您开始看到它的美妙之处了。当您超越一个字节的输入时,这一点变得更加明显,但我们将让您通过未来的挑战自己去发现!

现在,让我们谈谈表示法。您如何区分十进制中的 11、二进制中的 11(等于十进制中的 3)和十六进制中的 11(等于十进制中的 17)?对于数值常量,Python 的表示法是在二进制数据前加上 0b,十六进制前加上 0x,十进制保持不变,结果是 11 == 0b1011 == 0xb3 == 0b11 == 0x317 == 0b10001 == 0x11。但是对于 bytes,就像本挑战中一样,您可以使用转义序列来指定它们。转义序列以 \x 开头,后跟两个十六进制数字,从而在 bytes 常量中放入具有该值的单个字节!

有了这些知识,去迎接挑战,拿到flag吧!


有趣的事实: 其他一些可能有用的 Python 特性:

  • 如果您 print(n) 一个数字或使用 str(n) 将其转换为字符串,该数字将以 10 进制表示。
  • 您可以使用 hex(n) 获取数字的十六进制字符串表示。
  • 您可以使用 bin(n) 获取数字的二进制字符串表示。
  • 使用 int(s) 将字符串转换为数字会默认将其读取为 10 进制数字。
  • 您可以使用第二个参数指定不同的进制:int(s, 16) 会将字符串解释为十六进制,int(s, 2) 会将其解释为二进制。
  • 您可以尝试使用 int(s, 0) 自动识别数字进制,这需要在字符串上加前缀(二进制为 0b,十六进制为 0x,十进制不加)。
查看解析
cat /challenge/runme
#!/usr/bin/exec-suid -- /bin/python3 -I
import sys
print("Enter the password:")
entered_password = sys.stdin.buffer.read1()
correct_password = b"\xb9"
print(f"Read {len(entered_password)} bytes.")
entered_password = bytes.fromhex(entered_password.decode("l1"))
if entered_password == correct_password:
    print("Congrats! Here is your flag:")
    print(open("/flag").read().strip())
else:
    print("Incorrect!")
    sys.exit(1)
可知密码为\xb9,会将输入读取为字节,然后使用 decode("l1") 解码为字符串
echo -n 'b9' | /challenge/runme

More Hex 更多十六进制

您不限于两个十六进制数字!像十进制数字一样,您可以添加任意数量的它们来表示越来越多的字节。每两个十六进制数字就是一个额外的字节。出于好奇,一个十六进制数字被称为 nibble(嘿嘿!),但在指定数据时不使用这个单位。我们几乎总是在字节级别上处理数据,而不是更小的单位。

您在这一关中要做的是十六进制编码任意数据。也就是说,您将弄清楚您希望数据最终具有什么值,将该值编码为十六进制,然后发送十六进制字节。哇,您的第一次编码!

查看解析
cat /challenge/runme
#!/usr/bin/exec-suid -- /bin/python3 -I
import sys
print("Enter the password:")
entered_password = sys.stdin.buffer.read1()
correct_password = b"\x9d\x8b\xb7\xc5\xae\xcd\xf5\x97"
print(f"Read {len(entered_password)} bytes.")
entered_password = bytes.fromhex(entered_password.decode("l1"))
if entered_password == correct_password:
    print("Congrats! Here is your flag:")
    print(open("/flag").read().strip())
else:
    print("Incorrect!")
    sys.exit(1)
可知密码为\x9d\x8b\xb7\xc5\xae\xcd\xf5\x97,会将输入读取为字节,然后使用 decode("l1") 解码为字符串
echo -n '9d8bb7c5aecdf597' | /challenge/runme

Decoding Hex 解码十六进制

现在,让我们解码一些十六进制,而不是编码它。您能弄清楚程序需要什么吗?


注意: 这个挑战最棘手的部分之一是如何将原始二进制数据发送到它的标准输入。有几种方法可以做到这一点:

  1. 编写一个 Python 脚本将数据输出到标准输出,并将其通过管道传输到挑战的标准输入!这将涉及使用标准输出的原始字节接口:sys.stdout.buffer.write()
  2. 编写一个 Python 脚本来运行挑战并直接与之交互。我们推荐使用 pwntools:import pwnp = pwn.process("/challenge/runme")p.write(),和 p.readall()。一位 pwn.college 的校友创建了一个很棒的 pwntools 小抄,您可以参考。
  3. 对于一个越来越取巧的解决方案,echo -e -n "\xAA\xBB" 会将字节打印到标准输出,然后您可以通过管道传输。
查看解析
cat /challenge/runme
#!/usr/bin/exec-suid -- /bin/python3 -I
import sys
print("Enter the password:")
entered_password = sys.stdin.buffer.read1()
correct_password = b"80fbe4dea3a599b5"
print(f"Read {len(entered_password)} bytes.")
correct_password = bytes.fromhex(correct_password.decode("l1"))
if entered_password == correct_password:
    print("Congrats! Here is your flag:")
    print(open("/flag").read().strip())
else:
    print("Incorrect!")
    sys.exit(1)
可知密码为80fbe4dea3a599b5,会将输入读取为字节,然后使用 decode("l1") 解码为字符串
printf '\x80\xfb\xe4\xde\xa3\xa5\x99\xb5' | /challenge/runme
或echo -e -n '\x80\xfb\xe4\xde\xa3\xa5\x99\xb5' | /challenge/runme
或python3 -c "import sys; sys.stdout.buffer.write(b'\x80\xfb\xe4\xde\xa3\xa5\x99\xb5')" | /challenge/runme

Decoding Practice 解码练习

您脑子里能记住多少种进制?在这里,我们探索输入的二进制编码

查看解析
cat /challenge/runme
#!/usr/bin/exec-suid -- /bin/python3 -I
import sys
def decode_from_bits(s):
    s = s.decode("latin1")
    assert set(s) <= {"0", "1"}, "non-binary characters found in bitstream!"
    assert len(s) % 8 == 0, "must enter data in complete bytes (each byte is 8 bits)"
    return int.to_bytes(int(s, 2), length=len(s) // 8, byteorder="big")
print("Enter the password:")
entered_password = sys.stdin.buffer.read1()
correct_password = b"1100001011010100111010111111100010001001101111001111111010110111"
print(f"Read {len(entered_password)} bytes.")
correct_password = decode_from_bits(correct_password)
if entered_password == correct_password:
    print("Congrats! Here is your flag:")
    print(open("/flag").read().strip())
else:
    print("Incorrect!")
    sys.exit(1)
可知密码为1100001011010100111010111111100010001001101111001111111010110111,需要被解码为字节序列,然后与输入的字节比较
echo -e -n '\xc2\xd4\xeb\xf8\x89\xbc\xfe\xb7' | /challenge/runme

Encoding Practice 编码练习

现在轮到您了!

查看解析
cat /challenge/runme
#!/usr/bin/exec-suid -- /bin/python3 -I
import sys
def decode_from_bits(s):
    s = s.decode("latin1")
    assert set(s) <= {"0", "1"}, "non-binary characters found in bitstream!"
    assert len(s) % 8 == 0, "must enter data in complete bytes (each byte is 8 bits)"
    return int.to_bytes(int(s, 2), length=len(s) // 8, byteorder="big")
print("Enter the password:")
entered_password = sys.stdin.buffer.read1()
correct_password = b"\x9e\x94\xe0\xb8\x8d\x84\xa2\x8b"
print(f"Read {len(entered_password)} bytes.")
entered_password = decode_from_bits(entered_password)
if entered_password == correct_password:
    print("Congrats! Here is your flag:")
    print(open("/flag").read().strip())
else:
    print("Incorrect!")
    sys.exit(1)
可知密码为\x9e\x94\xe0\xb8\x8d\x84\xa2\x8b,脚本要求输入一个二进制字符串

echo -e -n '1001111010010100111000001011100010001101100001001010001010001011' | /challenge/runme

Hex-encoding ASCII 十六进制编码 ASCII

现在,让我们谈谈 str 字符串!在 Python 中,字符串是供人类使用的。字符串是人类可能写下、阅读、说出和梦想的字符序列。这包括诸如字母表中的字母,也包括诸如 🕴️ 之类的东西。当考虑到不同字母表的不同字母、不同的表情符号等等的数量时,很明显字符串的每个字符都有数千种不同的选项。

将人类可读的字符表示为内存中的一堆字节是另一种编码(这里用作名词)。一个字符,例如 🐉,通过内存中的字节被"编码"(这里用作动词),而这些字节"解码"为该字符。在"编码"和"解码"的这种用法中,数据实际上保持不变,但其解释发生了变化:编码由(比如说)您的命令行终端应用,以将程序发送的字节转换为您在屏幕上看到的字符和表情符号。

在 Python 中,您可以通过 my_string.encode()str 转换为其等效的 bytes。如果您有一堆字节想要解释为字符串,可以执行 my_bytes.decode()。但是字符串字符是如何映射到字节值的呢?

回到早期(比如 2000 年以前),当计算还不太国际化,人们仍然输入 :-) 而不是 🙂 时,人们并不真正担心单个字节可以表示的有限字符数。因此,早期的编码简单地将每个字符编码为一个字节,结果限制为 256 个可能的字符。由于早期计算主要在美国和西欧进行,最流行的这种编码,专门设计用于用各种字节值表示拉丁字母表中的字符,是 ASCII,其历史可以追溯到 1963 年(按计算标准来说是古老的历史!)。

ASCII 非常简单:每个字符一个字节,大写字母是 0x40+字母索引(例如,A 是 0x41,F 是 0x46,Z 是 0x5a),小写字母是 0x60+字母索引(a 是 0x61,f 是 0x66,z 是 0x7a),数字(是的,您看到的数字字符不是这些值的字节,它们是 ASCII 编码的数字字符)是 0x30+数字,所以 0 是 0x30,7 是 0x37。有用的特殊字符散布在映射周围:正斜杠(/0x2f),空格是 0x20,换行符是 0x0a。由于早期的计算先驱们是边做边编,一些 ASCII 字符并不是真正的字符:0x07 是一个响铃;当它被"打印"出来时,它真的会让您的终端发出哔哔声!其他"控制字符"做其他古怪的事情:例如,0x08删除最后一个字符,而不是本身作为一个字符。

低于 0x80128)的字节值,被认为是"标准 ASCII",即使在非英语国家也几乎是普遍定义的。您可以使用 man ascii 查看整个标准 ASCII 定义!您也可以在 python 中使用标准 ASCII 来编码字符串:my_string.encode("ascii")。但要小心,标准 ASCII 没有定义 0x80 以上的值,所以如果您解码具有这些值的字节,将会得到一个异常!例如,这行不通:b"\x80".decode("ascii")

高于 0x80 的值("扩展 ASCII")被不同国家用于自己的字符,由于字节值冲突导致了一些混乱。在美国,典型的"扩展 ASCII"编码被称为 Latin 1,它为 256 个可能的字节值中的每一个定义了一个字符。这对我们很有用,因为我们可以使用 "latin1" 在 Python 的字节和字符串之间轻松转换,包括:b"\x80".decode("latin1")

在这个挑战中,我们希望您给我们 ASCII 编码的十六进制值(有趣的事实:用十六进制指定字节值被称为"十六进制编码"!),我们将根据密码进行匹配。祝你好运!


注意: 当您阅读挑战以了解需要发送什么值时,您会注意到为 correct_password 指定的 bytes 常量的某些部分看起来……很奇怪。correct_password 中的每个字节代表内存中的一个字节,但它们通常仍然包含有用的、与人类相关的信息。虽然使用转义序列打印每个字节是有效的,但对人类来说并不那么有用,即使字节并不真正是供人类使用的。因此,Python 开发者决定将字节表示为……标准 ASCII!Python bytes 使用 ASCII 字符指定,较奇怪的"不可打印"字符(例如,任何超过 0x80 的字符和一些其他字符)使用 \x 转义序列指定。这对于普通字符也适用:\x41 愉快地编码了 A。其他一些特殊字符有自己特定的转义序列:例如,\n 编码一个换行符(相当于 \x0a)。您可以在 man ascii 中查看其他转义序列。因为 \ 被用作转义序列,Python(以及使用转义序列概念的几乎所有其他语言)必须也将实际的反斜杠指定为一个转义序列(具体来说,\\ 编码一个值为 0x5c\ 字节)。

好了,说了很多。去实践吧!

查看解析
cat /challenge/runme
#!/usr/bin/exec-suid -- /bin/python3 -I
import sys
print("Enter the password:")
entered_password = sys.stdin.buffer.read1()
correct_password = b"eewqcnzx"
print(f"Read {len(entered_password)} bytes.")
entered_password = bytes.fromhex(entered_password.decode("l1"))
if entered_password == correct_password:
    print("Congrats! Here is your flag:")
    print(open("/flag").read().strip())
else:
    print("Incorrect!")
    sys.exit(1)
可知密码为eewqcnzx,脚本会将输入的内容进行hex转码

echo -e -n '65657771636e7a78' | /challenge/runme

Nested Encoding 嵌套编码

好了,既然我们理解了字节是如何呈现给我们人类的,我们就可以玩更多编码练习了!让我们对我们的字节进行多重编码!

查看解析
cat /challenge/runme
#!/usr/bin/exec-suid -- /bin/python3 -I
import sys
try:
    entered_password = open(sys.argv[1], "rb").read()
except FileNotFoundError:
    print("Input file not found...")
    sys.exit(1)
correct_password = b"yqwunhqr"
print(f"Read {len(entered_password)} bytes.")
entered_password = bytes.fromhex(entered_password.decode("l1"))
entered_password = bytes.fromhex(entered_password.decode("l1"))
entered_password = bytes.fromhex(entered_password.decode("l1"))
entered_password = bytes.fromhex(entered_password.decode("l1"))
if entered_password == correct_password:
    print("Congrats! Here is your flag:")
    print(open("/flag").read().strip())
else:
    print("Incorrect!")
    sys.exit(1)
可知密码为yqwunhqr,脚本从传指定文件中读取输入,并且会将输入的内容进行四层hex转码

echo -n '33333337333333393333333733333331333333373333333733333337333333353333333633363335333333363333333833333337333333313333333733333332' > iqvn
/challenge/runme

Hex-encoding UTF-8 十六进制编码 UTF-8

一旦计算走向国际化并添加了表情符号,人们就需要能够同时使用超过 256 个可能的字符。在现代,这主要通过 UTF-8 编码得到了解决。UTF-8 是 Unicode 的一种特定的多字节编码,Unicode 是一个全球标准化的字符集,包含 essentially all characters known to humanity,加上您熟悉和喜爱的有趣表情符号。编码 Unicode 的方法有很多,UTF-8 是其中之一。Unicode(字符集)与 UTF-8(编码)的关系,就像英语(字符集)与标准 ASCII(编码)的关系一样。

方便的是,UTF-8 向后兼容标准 ASCII(例如,标准 ASCII 字节值在 UTF-8 中表示与 ASCII 中相同的字符),但在某些情况下会使用多于一个字节来表示单个字符。这允许 UTF-8 拥有 essentially limitless character options(它总是可以解释更多字节!):目前,它支持 well over 1,000,000 个字符!

UTF-8 是(默认情况下)Python 字符串的指定方式,所以您可以做诸如 my_string = "💥" 这样的事情)。您可以通过 my_string.encode("utf-8") 将其转换为实际的字节表示(因为它以字节形式存储在内存中),对于所讨论的表情符号,结果是字节 b'\xf0\x9f\x92\xa5'。这四个字节在 UTF-8 中代表那个表情符号。

在这个挑战中,您将学习制作表情符号字节。我们希望您创建代表 UTF-8 表情符号的原始字节,对其进行十六进制编码,并将这些十六进制值发送给我们。您能做到吗?


DOJO 注意: 由于 GUI 桌面终端中 Unicode 显示的错误,我们建议您对此挑战(以及任何其他依赖表情符号的挑战!)使用 VSCode 工作区。

查看解析
cat /challenge/runme
#!/usr/bin/exec-suid -- /bin/python3 -I
import sys
try:
    entered_password = open(sys.argv[1], "rb").read()
except FileNotFoundError:
    print("Input file not found...")
    sys.exit(1)
correct_password = "🐰 🏬 🚝 🔬".encode("utf-8")
print(f"Read {len(entered_password)} bytes.")
entered_password = bytes.fromhex(entered_password.decode("l1"))
if entered_password == correct_password:
    print("Congrats! Here is your flag:")
    print(open("/flag").read().strip())
else:
    print("Incorrect!")
    sys.exit(1)
可知密码为🐰 🏬 🚝 🔬的 UTF-8 编码字节序列

echo -n 'f09f90b020f09f8fac20f09f9a9d20f09f94ac' > iqvn
/challenge/runme iqvn

UTF Mixups UTF 混淆

UTF-8 是当前编码界的王者。例如,它被互联网上绝大多数网站使用。

但它并不是唯一的选择。在网络之外,其他编码也大量存在。由于各种(被误导的)技术原因,Windows 系统经常使用不同的 Unicode 编码:UTF-16。这种编码使用不同的字节值表示相同的 Unicode 字符! Needless to say, this leads to much confusion, and occasionally, security vulnerabilities。

编码混淆导致安全漏洞的一种常见方式是,对数据执行安全检查时错误地解码,然后在实际执行安全敏感操作时正确(且不同地)解码。如果安全检查是在错误的数据上执行的,那么危险的数据可能会被遗漏。

本挑战就是这种情况。您能获得 flag 吗?


DOJO 注意: 由于 GUI 桌面终端中 Unicode 显示的错误,我们建议您对此挑战(以及任何其他依赖表情符号的挑战!)使用 VSCode 工作区。

查看解析
cat /challenge/runme
#!/usr/bin/exec-suid -- /bin/python3 -I
import sys
try:
    entered_password = open("iitt", "rb").read()
except FileNotFoundError:
    print("Input file not found...")
    sys.exit(1)
correct_password = b"bjtpzmfh"
print(f"Read {len(entered_password)} bytes.")
assert entered_password != correct_password
entered_password = entered_password.decode("utf-16")
entered_password = entered_password.encode("latin1")
if entered_password == correct_password:
    print("Congrats! Here is your flag:")
    print(open("/flag").read().strip())
else:
    print("Incorrect!")
    sys.exit(1)
可知密码为bjtpzmfh,并且输入的密码会以utf-16解码

echo -e -n '\x62\x00\x6a\x00\x74\x00\x70\x00\x7a\x00\x6d\x00\x66\x00\x68\x00' > iitt
/challenge/runme iitt

Modifying Encoded Data 修改编码数据

到目前为止,我们已经看到了几种编码类型:UTF-8、UTF-16、扩展 ASCII(latin-1)和十六进制编码。这种编码转换数据,无论是一个概念(如 🎈 表情符号字符)还是内存中的实际字节,转换成其他字节。当您弄乱编码数据时会发生什么?没什么好事!在 UTF-8 中,🎈 编码为:

hacker@dojo:~$ ipython
In [1]: "🎈".encode("utf-8")
Out[1]: b'\xf0\x9f\x8e\x88'

如果我们弄乱结果字节,然后解码它们,我们(当然)会得到不同的东西:

In [2]: b'\xf0\x9f\x8e\xaa'.decode("utf-8")
Out[2]: '🎪'

In [3]: b'\xf0\x9f\x8e\x42'.decode("utf-8")
---------------------------------------------------------------------------
UnicodeDecodeError                        Traceback (most recent call last)
Cell In[3], line 1
----> 1 b'\xf0\x9f\x8e\x42'.decode("utf-8")

UnicodeDecodeError: 'utf-8' codec can't decode bytes in position 0-2: invalid continuation byte

第一次修改导致了一个不同的表情符号,第二次则出错了。根据编码的不同,并非所有字节值都能被正确解码!对于 UTF-8,这是由于指定数据的复杂算法。对于十六进制编码,这是因为只有数字 0 到 9 和字母 A 到 F 在十六进制中是有效的!

尽管如此,任何编码都可以在某种程度上被弄乱,正如我们在上面的第一个例子中看到的那样。当安全漏洞允许数据被破坏时,这可能使攻击者能够精心转换数据以达到其目的。我们将在 pwn.college 的后面学习如何保护数据免受此影响,但现在,让我们通过看看当我们弄乱一些十六进制时会发生什么来练习这个概念!

查看解析
cat /challenge/runme
#!/usr/bin/exec-suid -- /bin/python3 -I
import sys
def reverse_string(s):
    return s[::-1]
print("Enter the password:")
entered_password = sys.stdin.buffer.read1()
correct_password = b"\xf1~\xe6P\xc0\x9a\x1f\xa6"
print(f"Read {len(entered_password)} bytes.")
entered_password = entered_password[::-1]
entered_password = bytes.fromhex(entered_password.decode("l1"))
if entered_password == correct_password:
    print("Congrats! Here is your flag:")
    print(open("/flag").read().strip())
else:
    print("Incorrect!")
    sys.exit(1)
可知密码为\xf1~\xe6P\xc0\x9a\x1f\xa6,并且输入的密码被反转

echo -e -n '6af1a90c056ee71f' | /challenge/runme
操作 期望的输入格式 处理方式 输出结果
Unescape 包含 \xNN 的 C 转义字符串 解释转义字符 → 变成真实 bytes 正确地得到原始字节
From Hex 纯净十六进制(只包含 0-9A-F) 每两字节一组转换为 byte 输入不是纯 hex 时失败

Decoding Base64 解码 Base64

ASCII 和 UTF-8 是非常特定数据的编码:文本(或类似文本的字符)。十六进制编码更通用,您可以将其应用于任何数据。我们可能使用像十六进制这样的编码的原因是通过某些难以写入任意二进制代码的媒介(例如一张纸或某些通信协议)来传输信息。然而,它的效率非常低:它通过为每个字节输出两个 ASCII 十六进制数字而使数据大小翻倍

十六进制效率低下的原因与它方便的原因类似:每个数字只有 4 位可用,并且由于每个输出字符数字需要 8 位来显示(在 ASCII 中),数据大小会翻倍。幸运的是,我们可以通过增加每个输出字符可以传达的位数来提高编码的效率。

"base64" 这个名字来源于每个输出字符使用 64 个字符这一事实。这些字符实际上可以变化,但标准的 base64 编码使用大写字母 AZ、小写字母 az、数字 09 以及 +/ 符号的"字母表"。这总共产生 64 个输出符号,每个符号可以编码 2**6(2 的 6 次方)个可能的输入符号,即 6 位数据。这意味着要编码一个字节(8 位)的输入,您需要不止一个 base64 输出字符。事实上,您需要两个:一个编码前 6 位,一个编码剩余的 2 位(第二个输出字符的 4 位未使用)。为了标记这些未使用的位,base64 编码的数据为每两个未使用的位附加一个 =。例如:

hacker@dojo:~$ echo -n A | base64
QQ==
hacker@dojo:~$ echo -n AA | base64
QUE=
hacker@dojo:~$ echo -n AAA | base64
QUFB
hacker@dojo:~$ echo -n AAAA | base64
QUFBQQ==
hacker@dojo:~$

如您所见,3 个字节(3*8 == 24 位)精确编码成 4 个 base64 字符(4*6 == 24 位)。

base64 是一种流行的编码,因为它可以表示任何数据,而无需使用"棘手"的字符,如换行符、空格、引号、分号、不可打印的特殊字符等。这些字符在某些情况下会引起麻烦,而对数据进行 base64 编码可以很好地避免这种情况。

您还探索了其他"base"编码:base2 是二进制,base16 是十六进制!

现在,去解码获取 flag 吧!


提示: 您可以使用 Python 的 base64 模块(注意:此模块中的 base64 解码函数使用并返回 Python 字节)或 base64 命令行实用程序来完成此操作!

有趣的事实: pwn.college{FLAG} 中的 flag 数据实际上是 base64 编码的密文。您离能够构建类似道场的东西已经很近了!

查看解析
cat /challenge/runme
#!/usr/bin/exec-suid -- /bin/python3 -I
import sys
import base64
print("Enter the password:")
entered_password = sys.stdin.buffer.read1()
correct_password = b"+ROoXPZ/TAg="
print(f"Read {len(entered_password)} bytes.")
correct_password = base64.b64decode(correct_password.decode("l1"))
if entered_password == correct_password:
    print("Congrats! Here is your flag:")
    print(open("/flag").read().strip())
else:
    print("Incorrect!")
    sys.exit(1)
可知密码为+ROoXPZ/TAg=经base64解码得到的值
echo -e -n '+ROoXPZ/TAg=' | base64 -d | /challenge/runme

Encoding Base64 编码 Base64

不用说,您也可以将东西编码为 base64!现在就去这样做吧!

查看解析
cat /challenge/runme
#!/usr/bin/exec-suid -- /bin/python3 -I
import sys
import base64
print("Enter the password:")
entered_password = sys.stdin.buffer.read1()
correct_password = b'O"\x91\x7f5\xc1\x00n'
print(f"Read {len(entered_password)} bytes.")
entered_password = base64.b64decode(entered_password.decode("l1"))
if entered_password == correct_password:
    print("Congrats! Here is your flag:")
    print(open("/flag").read().strip())
else:
    print("Incorrect!")
    sys.exit(1)
可知密码为O"\x91\x7f5\xc1\x00n,输入会进行base64编码

echo -e -n 'TyKRfzXBAG4=' | /challenge/runme

Dealing with Obfuscation 处理混淆

安全意识薄弱的开发人员经常使用基于编码的混淆来代替加密。这种混淆通常无法阻止坚定的黑客访问相关数据,尤其是在他们阅读了实现它的软件逻辑之后。请设身处地地为这样的黑客着想,并获取这个 flag。

记住:"好的艺术家模仿,伟大的艺术家偷窃!" 当您进行安全分析并需要与定制软件交互时,从该软件中提取自定义通信协议的实现是实现互操作性的好方法。在这里试一试吧!

查看解析
cat /challenge/runme
#!/usr/bin/exec-suid -- /bin/python3 -I
import sys
import base64
def encode_to_bits(s):
    return b"".join(format(c, "08b").encode("latin1") for c in s)
print("Enter the password:")
entered_password = sys.stdin.buffer.read1()
correct_password = b"F(\x9f\xdf\xe2\xc6\x9fX"
print(f"Read {len(entered_password)} bytes.")
correct_password = base64.b64encode(correct_password)
correct_password = base64.b64encode(correct_password)
correct_password = encode_to_bits(correct_password)
correct_password = correct_password.hex().encode("l1")
if entered_password == correct_password:
    print("Congrats! Here is your flag:")
    print(open("/flag").read().strip())
else:
    print("Incorrect!")
    sys.exit(1)
可知初始密码为F(\x9f\xdf\xe2\xc6\x9fX,会进行两次base64编码、转换为二进制、转换为16进制

echo -e -n '3031303130313031303131303131303130313130313130303031313130303030303130313130313030313130313031303031303031313031303131313030313030313031303130303031303030313031303131303031303030313131303130313031303031313031303130313031313130313130303031313030313131303031' | /challenge/runme

Dealing with Obfuscation 2 处理混淆 2

您能走得更远吗?

查看解析
cat /challenge/runme
#!/usr/bin/exec-suid -- /bin/python3 -I
import sys
import base64
def reverse_string(s):
    return s[::-1]
def encode_to_bits(s):
    return b"".join(format(c, "08b").encode("latin1") for c in s)
print("Enter the password:")
entered_password = sys.stdin.buffer.read1()
correct_password = b"\x08\x9e\xb3zP\x03\x02\x03"
print(f"Read {len(entered_password)} bytes.")
entered_password = entered_password[::-1]
entered_password = bytes.fromhex(entered_password.decode("l1"))
entered_password = bytes.fromhex(entered_password.decode("l1"))
entered_password = bytes.fromhex(entered_password.decode("l1"))
correct_password = base64.b64encode(correct_password)
correct_password = correct_password.hex().encode("l1")
correct_password = encode_to_bits(correct_password)
correct_password = correct_password[::-1]
if entered_password == correct_password:
    print("Congrats! Here is your flag:")
    print(open("/flag").read().strip())
else:
    print("Incorrect!")
    sys.exit(1)
可知初始密码为\x08\x9e\xb3zP\x03\x02\x03,会进行base64编码、转换为16进制、转换为二进制、逆序;

输入内容会进行逆序、三次hex解码

echo -e -n '033333330333333313333333133333330333333313333333033333330333333303333333033333331333333313333333033333330333333313333333133333330333333303333333133333331333333303333333133333330333333303333333033333331333333313333333033333330333333303333333033333331333333303333333033333331333333313333333033333330333333313333333133333330333333303333333133333331333333303333333133333331333333303333333033333330333333313333333133333330333333313333333133333331333333303333333133333331333333303333333033333330333333303333333133333330333333303333333133333331333333303333333133333331333333303333333033333330333333313333333133333330333333313333333033333331333333303333333033333331333333313333333033333331333333313333333033333330333333313333333133333330333333303333333033333331333333313333333033333330333333313333333133333330333333313333333033333330333333303333333033333331333333313333333033333330333333303333333133333330333333303333333133333331333333303333333133333330333333303333333033333330333333313333333133333330333333313333333033333330333333303333333033333331333333313333333033333331333333303333333033333330333333303333333133333331333333303333333033333330333333313333333033333330333333313333333133333330333333313333333133333330333333303333333033333331333333313333333033333331333333313333333133333330333333303333333133333331333333303333333133333330333333303333333033333331333333313333333033333330333333313333333033333330333333303333333033333331333333313333333033333330333333313333333133333330333333313333333133333330333333303333333133333330333333303333333' | /challenge/runme

Talking Web 网络交互

Your First HTTP Request 你的第一个 HTTP 请求

显然,既然你正在用网页浏览器访问这个网站,这并非你第一次发出 HTTP 请求。但这是你为 pwn.college 挑战发起的第一个 HTTP 请求!运行 /challenge/server,在 dojo 工作区中启动 Firefox(为此你需要使用 GUI 桌面),然后访问它正在监听的 URL 以获取 flag!

查看解析
/challenge/server

Reading Flask 阅读 Flask 代码

太棒了,你已经掌握了基本流程。不过,还有一件事你需要做:你必须阅读并理解挑战的源代码!Web 服务器会将 HTTP 请求路由到不同的端点http://challenge.localhost/pwn 可能会指向处理请求路径 /pwn 的端点,而 http://challenge.localhost/college 可能会指向处理请求路径 /college 的端点。这个挑战有一个随机选择的端点名称。你必须阅读 /challenge/server 中的代码,理解它,并找出要在浏览器中访问哪个端点!


感到困惑?我们的 Web 服务器是使用 flask 库实现的。阅读其文档以建立对代码的理解,或者进行实验!

查看解析
cat /challenge/server
可以发现其中的端点为/qualify

Commented Data 注释中的数据

HTTP 是超文本传输协议。"超文本"这个命名源于 20 世纪末的技术乐观主义,它指的是不仅包含其含义,还携带关于如何被理解的附加数据的文本。在现代,这是通过各种方式实现的:HTTP 被用来传输许多不同类型的资源,而你的网页浏览器将它们组合起来,构建出你所看到并与之交互的网站。其中最古老的就是超文本标记语言,即 HTML。

HTML 以一种浏览器可以解释的方式,描述了(最初)应该出现在网页上的元素。我们将在后续模块中深入探讨 HTML 的微妙之处,但在这里,我们将练习穿透网站的表象,查看其背后的 HTML。和之前一样,你需要找到端点并在 dojo 内置的浏览器中访问它。但是,发送过来的 HTML 会隐藏 flag。你需要弄清楚如何查看 HTML 的页面源代码,而不是渲染后的结果,以获取这些隐藏的数据。


提示: 点击 Firefox 的"三"菜单(≡),然后转到 更多工具

查看解析
cat /challenge/server
可以发现其中的端点为/progress
其中页面的注释中藏有flag

HTTP Metadata HTTP 元数据

HTTP 促进了数据(例如 /challenge/server 发送给你的 HTML)和元数据(关于数据的数据)的传输。后者通过头部发送:HTTP 请求或响应中的字段,用于向服务器或浏览器提供额外指令。在这个例子中,flag 位于一个头部中。你能找到它吗?


提示: 你可以使用 Firefox 的 Web 开发者工具(≡,然后选择 更多工具)来检查头部。工具的"网络"选项卡显示所有的 HTTP 连接(你可能需要在打开 Web 开发者工具后重新加载页面,连接才会显示)。每个连接都有一个"头部"子选项卡,其中显示你的浏览器随请求发送的头部(请求头部)以及随响应接收到的头部(响应头部)。在那里找到 flag 头部!

查看解析
cat /challenge/server
可以发现其中的端点为/evaluate
其中响应包中藏有flag

HTTP (netcat) 使用 Netcat 发起 HTTP 请求

image-20241022140325886

你已经学会了如何发起 HTTP 请求(当然,可能你人生中大部分时间都在这么做!)。现在,让我们学习如何真正地发起 HTTP 请求。HTTP 协议本身,即在网络上传输的确切数据,实际上出人意料地易于人类阅读和编写。在这个挑战中,你将学习编写它。本挑战要求你使用一个名为 "netcat"(命令名:nc)的程序,这是一个通过网络连接进行通信的简单程序。Netcat 的基本用法涉及两个参数:主机名(服务器正在监听的位置,例如 Google 的是 www.google.com)和端口(标准的 HTTP 端口是 80)。

当 netcat 启动时,它会连接到服务器,并为你提供一个与它通信的原始通道。你将直接与 Web 服务器对话,没有中间层!这有多酷?

回顾讲座内容,找到 HTTP 请求的格式,并向 / 端点发起一个 GET 请求(我们稍后会处理更多端点)以获取 flag!


提示: 无法判断 netcat 是否连接成功?使用 -v 标志来开启一些详细输出!

提示: 输入了 GET 请求,但按 Enter 后没有任何反应?HTTP 请求以两个换行符终止。尝试再按一次 Enter!

思考一下… 直到此刻,你是否曾真正地发起过 HTTP 请求?

查看解析
/challenge/server
开启新终端
nc 127.0.0.1 80
GET / HTTP/1.1
使用`nc`尝试连接到本地计算机(127.0.0.1)的80端口
并构造HTTP GET请求包

HTTP Paths (netcat) 使用 Netcat 访问 HTTP 路径

好了,你已经掌握了 netcat 的基础知识。现在,向一个特定的路径发起 GET 请求!和往常一样,查看 /challenge/server 代码以了解更多信息。

查看解析
/challenge/server
开启新终端
nc 127.0.0.1 80
GET /hack HTTP/1.1

HTTP (curl) 使用 Curl 发起 HTTP 请求

接下来,我们将练习使用最常用的 HTTP 命令行工具之一:curl 来发起 HTTP 请求。与 netcat 不同,curl 是专门为 HTTP 设计的,你不需要编写原始的 HTTP 命令。相反,你必须学会使用正确的程序选项来实现你想要的。在这里,你只需向正确的端点发起一个 GET 请求!

查看解析
/challenge/server
开启新终端
curl 127.0.0.1/validate
使用`curl`向本地服务器(即127.0.0.1)发送一个 HTTP GET 请求

HTTP (python) 使用 Python 发起 HTTP 请求

最后,我们将学习我们 HTTP 工具库中的第四个工具:Python 的 requests 库。这个工具,连同浏览器,很可能将成为你的 HTTP 工具库中使用最频繁的两个工具。Requests 允许你编写复杂的 Web 交互脚本,这对于以后执行棘手的攻击是必要的。目前,事情很简单:打开 Python,import requests,然后 GET 那个 flag!

查看解析
/challenge/server
开启新终端
python
import requests  # 导入 requests 库,用于发送 HTTP 请求
response = requests.get('http://127.0.0.1/entry')  # 发送 GET 请求到指定的地址
print(response.text)	# 打印响应的文本内容

HTTP Host Header (python) 使用 Python 设置 HTTP Host 头

不幸的是,现代互联网的大部分运行在少数几家公司的基础设施上,而这些公司运行的某一台服务器可能需要为几十个不同的域名提供网站服务。服务器如何决定提供哪个网站呢?答案是 Host 头。

Host 头是一个由客户端(例如浏览器、curl 等)发送的请求头,通常等于在 HTTP 请求中输入的主机名。当你访问 https://pwn.college 时,你的浏览器会自动将 Host 头设置为 pwn.college,因此我们的服务器知道为你提供 pwn.college 网站,而不是其他东西。

到目前为止,你一直在交互的挑战都是不区分 Host 的。现在它们开始检查了。设置正确的 Host 头并获取 flag!

查看解析
/challenge/server
开启新终端
vim nihao.py
import requests  # 导入 requests 库,用于发送 HTTP 请求
headers = {
    'Host': 'flaws2.cloud'
}	# 自定义主机标头
response = requests.get('http://127.0.0.1/request', headers=headers)  # 发送 GET 请求,包含自定义主机标头
print(response.text)  # 打印响应的文本内容
python3 nihao.py

HTTP Host Header (curl) 使用 Curl 设置 HTTP Host 头

现在,让我们学习如何在 curl 中设置 Host 头!阅读它的 man 手册页,了解如何设置头部。

查看解析
/challenge/server
开启新终端
curl 127.0.0.1/gateway -H "Host: xss.pwnfunction.com"
使用`-H`参数对127.0.0.1发送的 HTTP GET 请求添加提示给出的主机标头

HTTP Host Header (netcat) 使用 Netcat 设置 HTTP Host 头

最后,你可以学习 Host 是如何在 netcat 中实际通过网络发送的。这可能会有点棘手。你实际上可以把 curl 当作信息来源!Curl 的 -v 选项会使其打印出它正在发送(以及接收到的)的确切头部。观察它,用 netcat 复制,然后获取 flag!

查看解析
/challenge/server
开启新终端
nc 127.0.0.1 80
GET /fulfill HTTP/1.1
Host:0xf.at:80

URL Encoding (netcat) 使用 Netcat 进行 URL 编码

还记得 HTTP 请求包含由空格分隔的字段吗?例如:GET /solve HTTP/1.1。如果路径(例如,不是 /solve)内部包含空格怎么办?这是合理的情况,因为这些路径通常引用目录,而目录名中可能包含空格!

如果放任不管,空格会搞乱 HTTP 请求。试想一个 HTTP 服务器试图理解 GET /solve my challenge HTTP/1.1。一个聪明的服务器或许能处理它,但一个只是逐词读取的版本很可能会把 my 当作 HTTP/1.1 来读然后崩溃!

为了避免这种情况,URL 使用 URL 编码 进行编码。与你之前在处理数据中看到的编码相比,这是一个简单的编码。任何棘手的字符(例如空格)都被简单地进行十六进制编码,并在前面加上一个 %。当然,因为 % 本身因此也成了一个棘手字符,所以它也必须被编码。在上面的例子中,/solve my challenge 会变成 /solve%20my%20challenge,因为 ASCII 空格字符的十六进制值是 0x20

总之,现在我们来练习一下。我们在端点中加入了空格。你还能拿到 flag 吗?


信息: 你会发现你也需要使用 curl 对 URL 进行编码(虽然我们不会让你绕这个弯子),方式完全相同。然而,Python 的 requests 库会自动为你进行 URL 编码。太有用了!

查看解析
/challenge/server
开启新终端
nc 127.0.0.1 80
GET /verify%20solve%20gate HTTP/1.1
Host: challenge.localhost:80

HTTP GET Parameters HTTP GET 参数

就像编程语言中的函数调用或 Shell 上的命令执行一样,HTTP 请求可以包含参数。GET 请求在 URL 的路径旁边,在 URL 的一个称为查询字符串的部分中发送参数。在这个挑战中,你将学习如何构造这个查询字符串。阅读挑战源代码以了解你需要什么参数,然后把它发送过来!你可以使用任何你想要的客户端:在所有客户端中,过程基本相同。


安全提示: 很容易将 HTTP 参数类比为函数调用的参数。然而,请记住:当你编写 C、Python 或 Java 代码时,攻击者(通常)不能只用随机参数调用你程序中的随机函数。但对于 HTTP,他们可以。他们可以随时随地发起 HTTP 请求!这已经造成了相当多的安全问题…

查看解析
/challenge/server
开启新终端
nc 127.0.0.1 80
GET /entry?pin=jgllilxh HTTP/1.1
Host: challenge.localhost:80

Multiple HTTP Parameters (netcat) 使用 Netcat 传递多个 HTTP 参数

当然,你可以传入多个参数;你只需要用 & 分隔它们:what=pwn&where=college。现在在 netcat 中试试看。

查看解析
/challenge/server
开启新终端
nc 127.0.0.1 80
GET /request?access=ejnskvxx&token=rmxwpdzo&signature=fhhmtasz HTTP/1.1
Host: challenge.localhost:80

Multiple HTTP Parameters (curl) 使用 Curl 传递多个 HTTP 参数

在 curl 中指定多个 HTTP 参数有点特殊,因为 & 在 Shell 中有特殊含义(它会在后台启动一个命令),如果你不小心,Shell 会被你的 & 干扰!确保将整个 URL(包括查询字符串)放在引号中以避免这种情况。现在试试看。

查看解析
/challenge/server
开启新终端
curl "127.0.0.1/challenge?access_code=ooyjsnxa&credential=tzgvbuqb&hash=ledxcvhf" -H "Host: challenge.localhost"
posted @ 2024-10-22 23:04  Super_Snow_Sword  阅读(427)  评论(0)    收藏  举报