
2025 SWPU-NSSCTF
前言
在nss刷题呢,刷到了这个比赛,这是新生赛,作为已经毕业的老玩家来说,这个难度很适合(bushi,纯粹为了保持手感罢了,希望自己还有当初的那一份热情吧,谁不是从小白萌新一步步爬上来的呢?权当记录几道有启发的题吧。
web
Do_you_know_http
这题X-FORWARD-FOR忘了,补一下知识。
当客户端通过代理(如 Nginx、Squid、CDN等)发起请求到服务器时,服务器通常只能看到代理服务器的 IP 地址,而无法获取客户端的真实 IP。为了解决这个问题,代理服务器会把客户端的真实 IP 添加到 X-Forwarded-For 头中,然后转发请求给后端服务器。
WebFTP
不要一味做题,要理解思路和细节的来龙去脉。
这题很有意思,先用dirsearch扫,可以扫到一些git文件,网上有用githack进行提权的,但是这道题明显不是,然后通过扫到PHP相关讯息可以通过phpinfo里找到相关flag。
ping ping ping
这道题学到了一些shell的知识和技巧做个总结, shell的多个命令可以用分号隔开;
9为什么能过WAF对空格的检测? 可以看出zsh把它解析成了制表符和换行,
或者用反引号来内联执行
下面开始讲题,并构造payload
payload = ?ip=;ls
可以看到文件目录下有index和flag,尝试flag未果,决定看index,发现过滤空格,那么好应用上面的知识进行过waf。
?ip=127.0.0.1;cat$IFS$7index.php
发现有对flag的正则过滤。
preg_match("/.*f.*l.*a.*g.*/", $ip) # 解释一下,这个匹配是按照出现的flag四个字母的出现顺序进行过滤的,所以可以考虑替换的方式
?ip=127.0.0.1;d=g;cat$IFS$9fla$d.php
即可爆出flag
easyupload1.0
传一句话木马上去,然后用antsword连,目录里的是假flag,最后flag在phpinfo里面。POST
a=phpinfo();
easyupload2.0
考的是php后缀,gpt给出如下答案:
在 PHP 开发中,合法的 PHP 文件后缀名主要有以下几种: 1. php 最标准、最常用的后缀名。所有的 PHP 源代码文件推荐使用此后缀。 2. .php3 早期 PHP 3 版本遗留下来的后缀,现在已经很少使用。 3. .php4 早期 PHP 4 版本遗留下来的后缀,现在也极少使用。 4. .php5 用于 PHP 5 版本时期的文件,现已不推荐使用。 5. .phtml 曾经流行的 PHP 后缀名,多用于嵌入 HTML 代码的 PHP 文件,现在比较少见。 6. .phps 展示 PHP 源代码时的一种后缀(比如源码高亮),服务器一般会将 .phps 文件源码高亮后输出而不是当作 PHP 解释执行。
这道题用的是phtml解决
easyupload3.0
考点:图片马+htaccess
图片马的制作方法:
cat xx.php >> xx.png
然后就是GIF89a(直接开burp在里面改)
上传后并不能直接getshell,要想办法把图片解析成php。 观察到server那边跑的是apache,这边介绍一下htaccess,是一个可以控制子目录或者主目录的解析形式的一种配置文件。
AddType application/x-httpd-phd .jpg// 意思是所有后缀jpg全部当作phd解析
如何判断shell解析成功?可以直接打开对应目录下的文件这里传入的是jpg。 后面也就一样咯,flag在上一级目录下。
ez_ez_php
考点:考的是php伪协议
php://
看代码可以得到要get一个参数flie=,且必须要php开头,我直接访问flag.php获得提示,真正的flag在flag处。
那么就构造payload就好了,
file=php://filter/convert.base64-encode/resource=flag #对于这道题是否base64影响不大
finalrce
考点:转义字符
\
的使用,以及tee命令的使用。
`preg_match('/bash|nc|wget|ping|ls|cat|more|less|phpinfo|base64|echo|php|python|mv|cp|la|\-|\*|\"|\>|\<|\%|\$/i',$url)`
题目的waf过滤常用的命令,但是可以用转义字符分割这些命令绕过,例如:
可以看出这样也是可以被shell识别的,那么我们构造第一个payload
?url=l\s / | tee output.txt
然后访问output可以获得根目录下的所有文件,flag文件也在这里,后面就是把flag文件打印出来了,注意题目有la
过滤,注意在这两个之间插一个转义字符。
pseudoProtocal
看标题就知道考的是伪协议了,这道题先用php的伪协议可以找到一个文件名很长的php文件,然后这个文件提示说要传入一个文件,开始试了php://input
发现没用,查了一下要传入data协议:
a=data:text/plain,I%20want%20flag
通过 data
协议,可以绕过对物理文件依赖的限制。
if (isset($a) && file_get_contents($a) === 'I want flag') {
echo "success\n";
echo $flag;
}
babyRCE
这道题有的过滤方式老生常谈了,
${IFS} 用来代替空格 \ 用来分隔命令 fla?.php (新学到的,在flag被过滤的情况下可以直接用问号来cat)
导弹迷踪
仔细看源码,不要偷懒!
babyphp
考察的是PHP函数和md5函数的绕过
第一个条件是intval(),去看文档发现数组可以返回1绕过,故可以设置a[]=1
,(这里注意数组的书写格式)
echo intval(array()), PHP_EOL; // 0echo intval(array('foo', 'bar')), PHP_EOL; // 1
第二个考察的是md5的强比较下的绕过,这里的策略是利用数组返回null的情况下进行绕过,所以传两个数组进行,b1[]=a&b2[]=b
第三个考察的是md5弱比较下的绕过,并且c1和c2必须是字符串,这里我们可以使用0e绕过,因为弱比较下会把字符串解析成整型比较,并且0e次方后面无论多少都是0,我们可以找一下常见的字符串哪些会解析成0e,
节选自关于md5的绕过技巧
- 有一些字符串的MD5值为0e开头,这里记录一下 - QNKCDZO - 240610708 - s878926199a - s155964671a - s214587387a- 还有MD5和双MD5以后的值都是0e开头的 - CbDLytmyGm2xQyaLNhWn - 770hQgrBOjrcqftrlaZk - 7r4lGXCH2Ksu2JNT3BYM
奇妙的MD5
这道题杂糅了几个md5的考点。
- ffifdyop
来源万能密码ffifdyop原理 - redfish999 - 博客园:
select * from 'admin' where password=md5($pass,true)
总而言之,就是这个字符串在经过md5加密后转ascii码起到一个sql注入的效果
后面的绕过,都使用数组绕过就行了。
高亮主题(划掉)背景查看器
考点:路径穿越,burp suite的爆破用法。
这道题的标题没有任何提示(我开始一直在想和主题的关系,
首先进去我发现POST有参数和值,然后没有头绪用dirsearch翻了一下,发现有flag
用get访问flag发现没有回显任何值,就此卡住。
原来POST那里才是真正的操作地方。
首先用burp suite抓包,fuzzing一波,选quick。不出意外的你可以在一个目录爆出密码,这就是正确目录,然后你把/etc之后的全部换成flag就拿到了
../../../../../etc/passwd # 我这里懒得敲了少了几个上级目录,建议你自己试试
ez_SSTI
考点:SSTI模版注入,总结一下大致是web服务器一些模版的输入口没有做过滤,可以根据情况执行一些RCE的内容,有点像SQL注入。
这道题可以用fenjing直接秒了。
具体原理学习这两个post不错,可以学习一下:
hardrce
考点:典型的无字母RCE
有很多方式可以绕过,这里可以取反,因为再取一次反就可以恢复。网上的脚本都是PHP写的,电脑上没环境,用python这样写。
def reverse_urlencode(s):
s = s.encode() # 转 bytes
s = bytes([~c & 0xFF for c in s]) # 每个字节取反
s = urlencode({'': s})[1:] # URL 编码并去掉开头的 '='
return s
wllm=(~%8C%86%8C%8B%9A%92)(~%93%8C%DF%D0); // system ls / 为什么要加system,因为php要这个才能执行命令
爆出来了flag文件继续cat就行,payload就不写了,直接拿内容
怎么多了个没用的php文件
考点:.user.ini
文件上传漏洞,测试发现只识别文件后缀名,改成图片的后缀嘛。(以后文件上传题先只做最小的改动,这是要注意的,不要直接传图片马,可能很多时候waf没有那么难过)
上传成功后,想到htaccess,但是我们怎么知道对面服务器运行的是什么,burpsuite可以冲返回报文的header那里看到server是nginx。
这时候可以介绍一下.user.ini了
`.user.ini` 是 **PHP** 的一个配置文件。当 PHP 以 **FastCGI** 模式(这是现在最常见的方式,例如与 Nginx 或 Apache 的 `mod_php` 不同)运行时,它会起作用。
它的核心特点是:**它是一种“每个目录”的INI文件**。这意味着你可以将 `.user.ini` 文件放在任何一个目录下,该文件中的配置指令会对其所在目录及其**所有子目录**中的PHP脚本生效。
这与全球级的 `php.ini` 和 Apache 的 `.htaccess` 有所不同,它提供了一种更细粒度的配置方式。
(全球级,看来只有AI能想出这种词)
那么我们就传个user.ini上去嘛,
auto_prepend_file=你文件名字.png
然后antsword连,注意你会发现连你自己的文件连不上去,回想一下题目,最后就能连上去了。
看看IP
一眼X-Forward-For,但是改127没有用啊。然后看了一眼WP。。
是直接把system命令嵌入X-Forwarded-For
{{system("ls /")}}{{system("cat /flag")}}
这是你可能一脸疑惑,为什么这样就做出来了。
经过我的推测,首先,我们可以通过 curl -l 来判断出后台是一个php,后面就可以顺理成章了。
crypto
base??
这题开始的思路有问题,发现value转key并不能解决问题,知晓了原来base64也是由一个dict进行映射的。而且base64生成的字符串长度始终能被4整除。
dict = {0: 'J', 1: 'K', 2: 'L', 3: 'M', 4: 'N', 5: 'O', 6: 'x', 7: 'y',
8: 'U', 9: 'V', 10: 'z', 11: 'A', 12: 'B', 13: 'C', 14: 'D', 15: 'E', 16: 'F', 17: 'G', 18: 'H', 19: '7', 20: '8', 21: '9', 22: 'P', 23: 'Q', 24: 'I', 25: 'a', 26: 'b', 27: 'c', 28: 'd', 29: 'e', 30: 'f', 31: 'g', 32: 'h', 33: 'i', 34: 'j', 35: 'k', 36: 'l', 37: 'm', 38: 'W', 39: 'X', 40: 'Y', 41: 'Z', 42: '0', 43: '1', 44: '2', 45: '3', 46: '4',
47: '5', 48: '6', 49: 'R', 50: 'S', 51: 'T', 52: 'n', 53: 'o', 54: 'p', 55: 'q', 56: 'r', 57: 's', 58: 't', 59: 'u', 60: 'v', 61: 'w', 62: '+', 63: '/', 64: '='}
ciphertext = "FlZNfnF6Qol6e9w17WwQQoGYBQCgIkGTa9w3IQKw"
# 1. 构建反向表(value 到 index)
decode_table = {}
for k,v in dict.items():
decode_table[v] = k
# 2. 将密文每个字符转为 index
indices = [decode_table[c] for c in ciphertext]
print(indices)
# 3. 用标准 base64 表('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/')映射
std_b64_table = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
std_b64_chars = [std_b64_table[i] for i in indices]
# 转为字符串
std_b64_str = ''.join(std_b64_chars)
print(std_b64_str)
import base64
print(base64.b64decode(std_b64_str))
RSA
RSA 介绍 - CTF Wiki 复习一下RSA,常考的考点了。 注意:pow的函数可以取三个参数。
from Crypto.Util.number import long_to_bytes
p = 12567387145159119014524309071236701639759988903138784984758783651292440613056150667165602473478042486784826835732833001151645545259394365039352263846276073
q = 12716692565364681652614824033831497167911028027478195947187437474380470205859949692107216740030921664273595734808349540612759651241456765149114895216695451
c = 108691165922055382844520116328228845767222921196922506468663428855093343772017986225285637996980678749662049989519029385165514816621011058462841314243727826941569954125384522233795629521155389745713798246071907492365062512521474965012924607857440577856404307124237116387085337087671914959900909379028727767057
e = 65537
n = p * q
# 欧拉函数
phi = (p - 1) * (q - 1)
# 计算私钥d,e^-1 mod phi
d = pow(e, -1, phi)
# 解密 c^d mod n
m = pow(c, d, n)
flag = long_to_bytes(m)
print(flag)
MISC
有一个GIF题就是盯帧
有一个emoji题挺有意思的,最后两个气球,我一度认为是base64,然后了解到了emoji-aes这个领域。原理就是利用AES加密(AES是一种对称加密,然后有密文可以解)实质上也是先加密,然后用emoji替换掉base64格式罢了
Reverse
Xor
先无脑F5,可以得到这个伪代码。
可以看出程序对flag的每一位进行了异或处理,然后判断是否等于enc_0这个数组里的每一位,然后继续在IDA查找enc_0中的元素。
好的,我们知道了数组的元素构成,接下来,就能还原成flag字符串了,什么?你不知道怎么还原,那么不妨试试重新异或回去吧,因为异或的性质就是这样的,你重新异或回去就可以得到原来的元素。
enc_0 = [0x34, 0x29, 0x29, 0x39, 0x2E, 0x3C, 0x01, 0x22, 0x15, 0x08, 0x25, 0x13, 0x09, 0x25, 0x18, 0x1B, 0x09, 0x13, 0x19, 0x25, 0x08, 0x1F, 0x0C, 0x1F, 0x08, 0x09, 0x1F, 0x07, 0x00, 0x00, 0x00, 0x00]
flag = ''.join(chr(c ^ 0x7A) for c in enc_0[:28]) # 只取前28个字节
print(flag)
base64
拖进ida看代码,发现做了一个base64 encode,但是放到cyberchef结果不对,点进b64_encode 函数,发现也很正常,然后卡住了。
这题的精髓在于替换了常规的b64 table,常规是: B64_TABLE = “ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/” # Base64 字符集
然后在cyberchef里替换掉对应的table就好了。
RC4
顾名思义,RC4解码,看代码有密文,点进去有加密后的数组。那么写个python脚本解密吧。
from Crypto.Cipher import ARC4
key = b"SakuraiCora"
print(key)
queue = bytes([
0xE8, 0x2B, 0x33, 0x25, 0xB2, 0x55, 0xE9, 0x0D, 0x5D, 0xAA,
0x69, 0xFD, 0x1B, 0x47, 0xD1, 0x7C, 0xA6, 0xFF, 0x52, 0xE1,
0x6C, 0xE8, 0x4C,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00
])
algorithm = ARC4.new(key)
flag = algorithm.decrypt(queue)
print(flag)
这道题可以学到,ARC4是对称加密,用一个密钥就可以同时加解密,所以这种加密方法不安全。
UPX
学到了,UPX壳,以及地址偏移
首先题目暗示了UPX壳,了解一下upx壳,这是一个常用的压缩壳也可以用来混淆反编译,直接把exe扔进kali里面 upx -d
后拉进ida反编译看代码。
新手小白很容看不懂这里的v6[j-8]的含义是什么,其实要注意头上面的变量以及注释,可以看到,v5和v6的地址是连着的,这里的-8即意味着v5,然后v6再继续10个bytes进行遍历。
下面写脚本进行还原,众所周知,异或是可以还原的,所以实际上我们把这几个拼接起来重新异或就好了
import struct
# v5 的值
v5 = 0xE013C2E39292934
v5_bytes = struct.pack('<Q', v5)
# v6 的前8字节
v6_part1 = 0xF25091325091312
v6_bytes1 = struct.pack('<Q', v6_part1)
print(v6_bytes1)
v6_part2 = 117574159
v6_bytes2 = struct.pack('<I', v6_part2)
# v5 与 v6 进行拼接
combine = v5_bytes + v6_bytes1 + v6_bytes2
# 对每个字节异或0x7A得到原始flag
flag_bytes = bytes([b ^ 0x7A for b in combine])
flag = flag_bytes.decode('ascii')
print("Flag:", flag)