HTB QuickR

题目描述:Let's see if you're a QuickR soldier as you pretend to be.
解题参考:https://sequr.be/blog/2020/05/quickr/
解题参考:https://0awawa0.medium.com/htb-quick-r-425b2012567c

本题为远程环境题目,在终端中通过nc进行访问。

image-20230609001238055

image-20230607105251440

如果直接扫描二维码的话,可以识别到结果为:

14.747533572766981 / 108.60719112210555 / 9.512112839301214 =

我们接下来需要做的就是计算这个数学等式的结果,并将结果输入提交即可拿到flag。

image-20230607105523041

但是我们发现提交时间太晚了,结合前面的提示 you got only 3 seconds!,我们只有3秒钟的时间去提交结果,而且如果提交错误的结果也是无法通过的。

image-20230607110441524

综上,我们只有3秒钟的时间去完成这一系列的操作才可以拿到flag,大致流程就是:

(1)打开连接,读取服务端响应的数据

(2)提取二维码

(3)识别二维码

(4)计算数学等式

(5)提交正确的计算结果

(6)接收flag

其中最复杂的一步就是提取二维码。下面我们逐步完成该题的解题步骤。

1.打开连接,读取服务端响应

我们可以通过 pwntools 模块与服务器进行交互,读取服务器传回的数据。

首先,我们使用 nc 测试与服务器交互的内容:

➜  2-QuickR nc 144.126.230.162 32190

   ___               _          __       _______
 .'   `.            (_)        [  |  _  |_   __ \
/  .-.  \  __   _   __   .---.  | | / ]   | |__) |
| |   | | [  | | | [  | / /'`\] | '' <    |  __ /
\  `-'  \_ | \_/ |, | | | \__.  | |`\ \  _| |  \ \_
 `.___.\__|'.__.'_/[___]'.___.'[__|  \_]|____| |___|



[*] Hello there! Let's see if you are an QuickR soldier, you got only 3 seconds!

...
QR code content...
...

[+] It's important to realise that this is, in a real sense, an illusion: you simply need the true machine value.
[!] Decoded string:

我们可以使用下面的代码来接收二维码部分的内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#python3 code : exp.py
#代码中字符串前面的b"",表示处理的该数据是字节流类型的数据

from pwn import *

p = remote('144.126.230.162', 32190)
p.recvuntil(b"you got only 3 seconds!")
p.recvuntil(b"\n\n\n")
data = p.recvuntil(b"\n\n").strip(b"\n\n")
print(data)   #ANSI转义字符表示的二维码
print(data.decode())  #decode()函数可以解码字节流数据为字符串格式,可以实现解析ANSI转义字符
p.close()

执行后的效果:

1
2
3
4
5
6
 python3 exp.py
[+] Opening connection to 144.126.230.162 on port 32190: Done
#(1)显示data变量存放的ANSI转义字符表示的二维码数据
b'\t\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\n\t\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1b[0m\x1b[7m  \x1
#(2)显示解析ANSI转义字符后显示的data变量存放的二维码
...有颜色的二维码图...

我们可以看到 data 变量中有很多的ANSI转义字符,通过对ANSI转义字符的解析得到了我们在终端中看到的二维码。

为了查看 data 变量中存放数据的规律,我们先将data变量存放到文件data.txt中,使用如下代码即可创建包含完整 data 变量数据的文件data.txt

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from pwn import *

p = remote('144.126.230.162', 32190)
p.recvuntil(b"you got only 3 seconds!")
p.recvuntil(b"\n\n\n")
data = p.recvuntil(b"\n\n").strip(b"\n\n")  #存放的ANSI转义字符表示的二维码数据
lines = data.split(b"\n")  #以"\n"为分隔符分割每一行
f = open("data.txt", "wb")
for line in lines:
	f.write(line+b"\n")
p.recvuntil(b"Decoded string:")
f.close()
p.close()

我们使用 sublime 打开data.txt进行查看(建议先关闭 sublime 的自动换行)。

image-20230608171437893

特征1:可以看到这部分数据一共有 53 行,实际包含数据的行数只有 51 行。

特征2:代码中有特别多的双空格(两个空格字符)。

image-20230608171658133

特征3:每个双空格左右两边都有一个ANSI转义字符。两个ANSI转义字符实现对一个双空格的包裹。

我们知道ANSI转义字符是可以对字符串设置颜色的。基本颜色表示如下:

image-20230608172303593

详细信息请查看:https://ss64.com/nt/syntax-ansi.html

其中,这里的Esc字符对应的的十六进制值就是我们看到的data.txt中大量出现的<0x1b>

也就是说在data.txt中出现了大量的Esc[7mEsc[0mEsc[41m

对应上表中实现的效果就是:Esc[7m:反转当前终端前景色(文字)的颜色、Esc[0m:重置终端为默认颜色、Esc[41m:设置背景色(字体下面的背景颜色)为红色。

可能只看描述的话感觉有点绕。下面我们在 python 环境中显示这些控制字符的效果。

①默认情况下,当前的演示终端颜色为黑底白字(黑色背景,白色字体)

image-20230608173730156

②接下来,我们使用Esc[7m反转当前前景色(文字)的颜色。也就是把前景色(文字)的颜色和背景色进行互换。也就是黑底白字反转为白底黑字。

image-20230608175622775

可以看到,此时已经实现了黑底白字变为白底黑字。

③继续,我们使用Esc[0m重置终端为默认颜色,也就是这里演示终端的黑底白字(黑色背景,白色字体)

image-20230608174727161

④继续,我们使用Esc[41m设置背景色(字体下面的背景颜色)为红色。

image-20230608175508535

上面的演示中,我们通过ANSI转义字符实现了对终端前景色和背景色的修改。改完一次之后,后续当前终端会一直使用这种配色方案。这是一种使用ANSI转义字符的用途。

⑤ANSI转义字符还可以实现为输出的字符串设置颜色。比如我们可以给字符串"hello"设置为红色字体。

image-20230608211742701

image-20230608211613133

我们设置完前景色红色Esc[31m后,紧接着设置了要显示的内容hello,注意字符串hello左右没有空格,最后,为了不影响后续的终端配色方案,我们还需要使用Esc[0m还原之前的终端配色方案。这种用法也是很常用的。

⑥回到题目里,我们可以看到,我们接收到的二维码数据部分第一行是白色的。

image-20230608212541010

那么这是怎么实现的呢?

查看我们保存的data.txt文件,我们可以看到第一行开始就是持续的重复的<0x1b>[7m <0x1b>[0m,也就是\x1b[7m \x1b[0m。注意两个ANSI转义字符之间的两个空格,本质上就是通过这两个ANSI转义字符为这个两个空格上色。因为当前演示的终端默认配色方案为黑底白字,那么这里的上色效果就是:先反转颜色,实现白底黑字,接着还原终端默认配色,最终实现了将这个双空格上色为白色的效果。

我们将第一行中的复制到新的记事本中,搜索关键字<0x1b>[7m <0x1b>[0m

image-20230608213327712

可以看到一共包含51组,也就是说二维码的第一行白色就是由这51组双空格设置为白色实现的。

相应的,对应data.txt的51行数据,那么就可以理解为这个二维码就是51*51像素块的数据。

我们还可以观察到,文件data.txt的前3行,后3行以及左侧3组和右侧组的都是相同的内容(<0x1b>[7m <0x1b>[0m),也就是都是白色的格子。

image-20230608213729301

刚好对应了二维码的显示效果。

image-20230608214141961

除了<0x1b>[7m <0x1b>[0m显示为白格子之外,还有一组显示为:<0x1b>[41m <0x1b>[0m,这组的显示效果就是渲染红格子。data.txt通过白格子和红格子的拼接,最终呈现出了一个完整的二维码。

2.提取二维码

明白了上面二维码实现的原理之后,我们就可以按照像素块的方式提取还原二维码了。

白色格子:<0x1b>[7m <0x1b>[0m

红色格子:<0x1b>[41m <0x1b>[0m

我们可以对每一行的内容进行匹配替换。匹配白色格子并替换为-,表示二维码的空白部分;匹配红色格子并替换为*,表示二维码的数据部分。

1
2
3
4
5
6
7
8
9
#code test : test.py
f = open("data.txt", "rb")
lines = f.readlines()
for line in lines:
	line = line.decode("utf-8")  #先将字节流转换为字符串类型,否则对数据进行后续的替换处理
	line = line.replace("\t", "").replace("\n", "")  #去除每一行中的"\t"和"\n"
	line = line.replace("\x1b[7m  \x1b[0m","-")  #替换白色格子为"-"
	line = line.replace("\x1b[41m  \x1b[0m","*") #替换红色格子为"*"
	print(line)

输出效果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
➜  2-QuickR python3 test.py
---------------------------------------------------
---------------------------------------------------
---------------------------------------------------
---*******-*--*-*---**-**-**-*-*-*--*--*-*******---
---*-----*-***--*-------*--*-*--*---*-*--*-----*---
---*-***-*-*-****-*--**--**-***-***-*-*--*-***-*---
---*-***-*---******-----*-----**----*-**-*-***-*---
---*-***-*--********-********-**-**--***-*-***-*---
---*-----*-*-*--*-*--***---****-*--------*-----*---
---*******-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*******---
-----------*****-***---*---*-*----***--*-----------
-----***-*-**----***--******-**-**--*-*-***--***---
----*--**-***-*-*--**-**-*-*-*---*-**--*---***-*---
------**-**--*--***-*---*---**-*-----*-****--------
---*--*-*--*-**---**-****-****-******-**---*-***---
-----*****--*--**-*-****-*-*******--*-*-*-*-*-**---
---**-**--**--*****-**-*****-----**-*--*---*****---
---*-**-***-*-------***-****---**-*--*--**--**-----
---*-**----****--*-*-*-*****--*-*-***-**---*-**----
----*-*--*-----**--*--***-*-*--*----**---**-*-*----
---***--*---**-**--*-***-***---**--**--*---*-***---
-----*---*---*---*---*-**---**-*-***-*-*-*---------
-------*--*-****--**--******-----***-----*-*-*-*---
----********-**-****-*******-*****--*********-*----
---**-**---*----***-**-*---*-**--**-*-**---*****---
-------*-*-*-*------*--*-*-**-*-**---*-*-*-*-------
---*-*-*---*-***-*-**-**---*--**-*---*-*---*-*-----
----********-*-**-*-*-******-**---**---******-**---
----**-*----*****--*-*--**-*---*---**-*-*-*--***---
----**--**--***--*-*--*-----**********-*--*--*-----
---***-**-*---**-**---*-**-**-*-*-**--****---*-----
-----*****-*-*--**--***-*-**-*---------*---**--*---
---**-**--*--*-----*-***--**-****---*-*---*--*-*---
-----******-*****--*-***-**--*--*-*--*----**-*-----
---***-*---*--*-********--**-*-*--**--**-*-*-*-----
-----**-**--***-*--******---***-*---**-*---**------
----**--*-*----**---**--*--**----*--*-*--**---**---
-------*-*--**----*---*----*-****--*-**-----**-----
----****---*-*-*--*----*-*--*-****-**-******-**----
---*--**-*-*------*-**-******-*-*----*-*****---*---
-----------***-*---*--**---****---*-*--*---*****---
---*******------****--**-*-*-*-------***-*-*-*-----
---*-----*----***-*-*--*---*----*-**---*---*-**----
---*-***-*-**-*****-********---*-----*-******-*----
---*-***-*-*-*-*-**-**---*--*-*-*--**-*-*--*--**---
---*-***-*-**---**----*--**-******---***-**-**-----
---*-----*--****--*-***-*--**--*-*--*--*---*-*-----
---*******--------**--*-*--*--*-----*--****-*-*----
---------------------------------------------------
---------------------------------------------------
---------------------------------------------------

继续修改下上面的代码,我们使用 Pillow 模块,将用*-表示的二维码,按照像素点填充的方式,将其转换为一张51*51像素的黑白图片。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#code test : test.py
from PIL import Image
from PIL import ImageDraw

f = open("data.txt", "rb")
lines = f.readlines()
img = Image.new("1", (51,51), 1) #创建了一个新的图像对象img
#Image.new(mode, size, color) 
#mode: 二值图像,只能是黑色(0)或白色(1)
#size: 51*51像素
#color: 指定了图像的初始颜色,1表示初始时图像中的所有像素块都是白色
draw = ImageDraw.Draw(img)       #创建一个用于在图像上进行绘制的对象draw

y = 0  #初始行数为0
for line in lines:
	line = line.decode("utf-8")  #先将字节流转换为字符串类型,否则对数据进行后续的替换处理
	line = line.replace("\t", "").replace("\n", "")  #去除每一行中的"\t"和"\n"
	line = line.replace("\x1b[7m  \x1b[0m","-")  #替换白色格子为"-"
	line = line.replace("\x1b[41m  \x1b[0m","*") #替换红色格子为"*"
	#print(line) 
	x = 0 #初始列数为0,每次开始新的行数,列数都重新从0开始
	for char in line:
		if char == "*":
			draw.point((x, y), 0)  #填充黑色像素块
		elif char == "-":
			draw.point((x, y), 1)  #填充白色像素块
		else:
			print("character error.")
		x += 1
	y += 1	
img.save("qr.png") #保存生成的二维码

f.close()

执行后,我们就可以在当前文件夹下找到新生成的二维码文件qr.png

1
➜  2-QuickR python3 test.py

image-20230608232038120

可以看到,我们已经成功的还原了二维码。

image-20230608232109652

接下来我们只需要将上面的代码进行稍微的调整下,就可以用在解题脚本exp.py中了。修改后的exp.py脚本内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
from PIL import Image
from PIL import ImageDraw
from pwn import *

p = remote('144.126.230.162', 32190)
p.recvuntil(b"you got only 3 seconds!")
p.recvuntil(b"\n\n\n")
data = p.recvuntil(b"\n\n").strip(b"\n\n")
lines = data.split(b"\n")  #以"\n"为分隔符分割每一行

#Image.new(mode, size, color) 
img = Image.new("1", (51,51), 1) #创建一个新的图像对象img
#Image.new(mode, size, color) 
#mode: 二值图像,只能是黑色(0)或白色(1)
#size: 51*51像素
#color: 指定了图像的初始颜色,1表示初始时图像中的所有像素块都是白色
draw = ImageDraw.Draw(img)       #创建一个用于在图像上进行绘制的对象draw

y = 0  #初始行数为0
for line in lines:
	line = line.decode("utf-8")  #先将字节流转换为字符串类型,否则对数据进行后续的替换处理
	line = line.replace("\t", "").replace("\n", "")  #去除每一行中的"\t"和"\n"
	line = line.replace("\x1b[7m  \x1b[0m","-")  #替换白色格子为"-"
	line = line.replace("\x1b[41m  \x1b[0m","*") #替换红色格子为"*"
	#print(line) 
	x = 0  #初始列数为0,每次开始新的行数,列数都重新从0开始
	for char in line:
		if char == "*":
			draw.point((x, y), 0)  #填充黑色像素块
		elif char == "-":
			draw.point((x, y), 1)  #填充白色像素块
		else:
			print("character error.")
		x += 1
	y += 1	
img.save("qr.png")  #保存生成的二维码

p.recvuntil(b"Decoded string:")
p.close()

运行上面的脚本,即可直接从服务端获取二维码数据,然后还原二维码到文件qr.png中。

1
2
3
➜  2-QuickR python3 exp.py
[+] Opening connection to 144.126.230.162 on port 32190: Done
[*] Closed connection to 144.126.230.162 port 32190

image-20230608233050757

3.识别二维码

接下来我们需要使用 pyzbar 模块识别图片qr.png中的二维码读取出要计算的数学等式。

macos安装pyzbar模块:

1
2
3
brew install zbar
pip install pyqrcode
pip install pyzbar

识别qr.png的代码:

1
2
3
4
5
6
7
8
9
#code test : test.py
from PIL import Image
from pyzbar.pyzbar import decode

equation = decode(Image.open("./qr.png"))
equation = equation[0].data.decode() #将字节流类型的数据转为字符串类型
equation = equation.replace("=","").replace("x","*") #去除等号,替换乘号

print(equation)

执行效果:

1
2
➜  2-QuickR py test.py
2.095213484743085 / 23.013727493106604 / 171.06434387764517

4.剩余步骤

剩下还有几步比较简单的操作步骤:计算数学等式、提交正确的计算结果、接收flag。

完整的解题脚本整理如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
from PIL import Image
from PIL import ImageDraw
from pyzbar.pyzbar import decode
from pwn import *

p = remote('144.126.230.162', 32190)
p.recvuntil(b"you got only 3 seconds!")
p.recvuntil(b"\n\n\n")
data = p.recvuntil(b"\n\n").strip(b"\n\n")
lines = data.split(b"\n")  #以"\n"为分隔符分割每一行

#Image.new(mode, size, color) 
img = Image.new("1", (51,51), 1) #创建一个新的图像对象img
#Image.new(mode, size, color) 
#mode: 二值图像,只能是黑色(0)或白色(1)
#size: 51*51像素
#color: 指定了图像的初始颜色,1表示初始时图像中的所有像素块都是白色
draw = ImageDraw.Draw(img)       #创建一个用于在图像上进行绘制的对象draw

y = 0  #初始行数为0
for line in lines:
	line = line.decode("utf-8")  #先将字节流转换为字符串类型,否则对数据进行后续的替换处理
	line = line.replace("\t", "").replace("\n", "")  #去除每一行中的"\t"和"\n"
	line = line.replace("\x1b[7m  \x1b[0m","-")  #替换白色格子为"-"
	line = line.replace("\x1b[41m  \x1b[0m","*") #替换红色格子为"*"
	#print(line) 
	x = 0  #初始列数为0,每次开始新的行数,列数都重新从0开始
	for char in line:
		if char == "*":
			draw.point((x, y), 0)  #填充黑色像素块
		elif char == "-":
			draw.point((x, y), 1)  #填充白色像素块
		else:
			print("character error.")
		x += 1
	y += 1	
img.save("qr.png")  #保存生成的二维码

equation = decode(Image.open("./qr.png"))
equation = equation[0].data.decode() #将字节流类型的数据转为字符串类型
equation = equation.replace("=","").replace("x","*") #去除等号,替换乘号
result = eval(equation)  #计算数学等式

p.recvuntil(b"Decoded string:")
p.sendline(str(result).encode())   #发送等式计算的结果给服务器
p.recv()
flag = p.recv().decode().split("flag:")[1]  #提取服务器返回的flag
print(flag)
p.close()

脚本运行结果:

image-20230609000941285

updatedupdated2023-12-052023-12-05