WEB
Ping
挺有意思的一道题
首先出题人是提供了源码的, 核心代码就是下面这些
def run_ping(ip_base64):
try:
decoded_ip = base64.b64decode(ip_base64).decode('utf-8')
if not re.match(r'^\d+\.\d+\.\d+\.\d+$', decoded_ip):
return False
if decoded_ip.count('.') != 3:
return False
if not all(0 <= int(part) < 256 for part in decoded_ip.split('.')):
return False
if not ipaddress.ip_address(decoded_ip):
return False
if len(decoded_ip) > 15:
return False
if not re.match(r'^[A-Za-z0-9+/=]+$', ip_base64):
return False
except Exception as e:
return False
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:
return False
基本上就是, 接收用户传入的一个字符串, 然后使用base64.b64decode(ip_base64).decode('utf-8')对这个字符串进行解码, 然后对解码后的结果进行各种检查. 如果通过了检查, 就将这个字符串放入echo "ping -c 1 $(echo '{ip_base64}' | base64 -d)" | sh里, 执行这个命令
可以看到, 检查确实是比较严格的, 基本上不可能直接绕过判断插入恶意代码
这里其实是要利用默认配置下base64.b64decode()与 Linux base64 的行为的不一致性
直接看看base64.b64decode()
# base64.py
...
def b64decode(s, altchars=None, validate=False):
s = _bytes_from_decode_data(s)
if altchars is not None:
altchars = _bytes_from_decode_data(altchars)
assert len(altchars) == 2, repr(altchars)
s = s.translate(bytes.maketrans(altchars, b'+/'))
return binascii.a2b_base64(s, strict_mode=validate)
...
注意, 默认情况下validate = False
跟进binascii.a2b_base64(s, strict_mode=validate)
// binascii_a2b_base64_impl() in binascii.c
binascii_a2b_base64_impl(PyObject *module, Py_buffer *data, int strict_mode)
{
...
if (quad_pos >= 2 && quad_pos + ++pads >= 4) {
// 当前四字符组已经处理了至少2个有效字符且有效字符数 + 填充字符数 >= 4
if (strict_mode && i + 1 < ascii_len) {
// 如果开启了严格模式,并且当前字符后面还有未处理的字符
state = get_binascii_state(module);
if (state) {
PyErr_SetString(state->Error, "Excess data after padding");
// 抛出“填充字符后有多余字符”的异常
}
goto error_end;
// 跳转到错误处理代码
}
goto done;
// 跳转到正常结束代码
}
...
}
这里的 strict_mode 就是 validate
在默认配置下, 也就是validate = False的情况下, python 的 base64.b64decode()不会处理填充符号后的其他字符, 因为在判断字符组已经处理了至少2个有效字符且有效字符数 + 填充字符数 >= 4 之后他直接 goto done 跳出了
也就是说
import base64
print(base64.b64decode("MS4xLjEuMQ==").decode('utf-8'))
print(base64.b64decode("MS4xLjEuMQ==dGVzdA==").decode('utf-8'))
# 1.1.1.1
# 1.1.1.1
但是, Linux base64 默认情况下会处理填充符号后的其他字符
echo 'MS4xLjEuMQ==' | base64 -d
echo 'MS4xLjEuMQ==dGVzdA==' | base64 -d
# 1.1.1.1
# 1.1.1.1test
利用这个差异分别 base64 编码某个ip地址 (编码后需要含有=填充符) 和要执行的命令, 然后拼接在一起即可 RCE 
online_unzipper
源码:
import ...
app = Flask(__name__)
app.secret_key = os.environ.get("FLASK_SECRET_KEY", "test_key")
UPLOAD_FOLDER = os.path.join(os.getcwd(), "uploads")
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
users = {}
...
@app.route("/upload", methods=["GET", "POST"])
def upload():
if "username" not in session:
return redirect(url_for("login"))
if request.method == "POST":
file = request.files["file"]
if not file:
return "未选择文件"
role = session["role"]
if role == "admin":
dirname = request.form.get("dirname") or str(uuid.uuid4())
else:
dirname = str(uuid.uuid4())
target_dir = os.path.join(UPLOAD_FOLDER, dirname)
os.makedirs(target_dir, exist_ok=True)
zip_path = os.path.join(target_dir, "upload.zip")
file.save(zip_path)
try:
os.system(f"unzip -o {zip_path} -d {target_dir}")
except:
return "解压失败,请检查文件格式"
os.remove(zip_path)
return f"解压完成!<br>下载地址: <a href='{url_for('download', folder=dirname)}'>{request.host_url}download/{dirname}</a>"
return render_template("upload.html")
@app.route("/download/<folder>")
def download(folder):
target_dir = os.path.join(UPLOAD_FOLDER, folder)
if not os.path.exists(target_dir):
abort(404)
files = os.listdir(target_dir)
return render_template("download.html", folder=folder, files=files)
@app.route("/download/<folder>/<filename>")
def download_file(folder, filename):
file_path = os.path.join(UPLOAD_FOLDER, folder, filename)
try:
with open(file_path, 'r') as file:
content = file.read()
return Response(
content,
mimetype="application/octet-stream",
headers={
"Content-Disposition": f"attachment; filename={filename}"
}
)
except FileNotFoundError:
return "File not found", 404
except Exception as e:
return f"Error: {str(e)}", 500
if __name__ == "__main__":
app.run(host="0.0.0.0")
稍微记一下思路:
先软链接读/proc/self/environ拿FLASK_SECRET_KEY, 伪造admin的session重新登陆, 配置 dirname = xxx/../ , 然后再传个包含到根目录的软链接的压缩包, 解压后利用@app.route("/download/<folder>")这个路由列出根目录各文件的文件名, 拿到文件名后即可利用@app.route("/download/<folder>/<filename>")这个路由下载flag
Peek a Fork
TODO
Unfinished
TODO
safenotes
TODO
