3626 words
18 minutes
SWPUCTF2025
2025-09-13

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的多个命令可以用分号隔开; screeshot.png IFSIFS9为什么能过WAF对空格的检测? 可以看出zsh把它解析成了制表符和换行, .png 或者用反引号来内联执行

下面开始讲题,并构造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

图片马的制作方法:

Terminal window
cat xx.php >> xx.png

然后就是GIF89a(直接开burp在里面改)

上传后并不能直接getshell,要想办法把图片解析成php。 观察到server那边跑的是apache,这边介绍一下htaccess,是一个可以控制子目录或者主目录的解析形式的一种配置文件。

image.png

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过滤常用的命令,但是可以用转义字符分割这些命令绕过,例如:

image.png

可以看出这样也是可以被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; // 0
echo 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的考点。

  1. ffifdyop

来源万能密码ffifdyop原理 - redfish999 - 博客园

select * from 'admin' where password=md5($pass,true)

总而言之,就是这个字符串在经过md5加密后转ascii码起到一个sql注入的效果

后面的绕过,都使用数组绕过就行了。

高亮主题(划掉)背景查看器#

考点:路径穿越,burp suite的爆破用法。

这道题的标题没有任何提示(我开始一直在想和主题的关系,

首先进去我发现POST有参数和值,然后没有头绪用dirsearch翻了一下,发现有flag

image.png

用get访问flag发现没有回显任何值,就此卡住。

原来POST那里才是真正的操作地方。

首先用burp suite抓包,fuzzing一波,选quick。不出意外的你可以在一个目录爆出密码,这就是正确目录,然后你把/etc之后的全部换成flag就拿到了

image.png

../../../../../etc/passwd # 我这里懒得敲了少了几个上级目录,建议你自己试试

ez_SSTI#

考点:SSTI模版注入,总结一下大致是web服务器一些模版的输入口没有做过滤,可以根据情况执行一些RCE的内容,有点像SQL注入。

这道题可以用fenjing直接秒了。

具体原理学习这两个post不错,可以学习一下:

SSTI之细说jinja2的常用构造及利用思路 - 蚁景网安实验室 - 博客园

1. SSTI(模板注入)漏洞(入门篇) - bmjoker - 博客园

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,可以得到这个伪代码。

.png

可以看出程序对flag的每一位进行了异或处理,然后判断是否等于enc_0这个数组里的每一位,然后继续在IDA查找enc_0中的元素。

.png

好的,我们知道了数组的元素构成,接下来,就能还原成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 字符集

.png

然后在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反编译看代码。

image.png

新手小白很容看不懂这里的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)

后面的逆向题有的需要debug调试,之后用Windows更嘿嘿。#

SWPUCTF2025
https://blog.jack1.me/posts/swpuctf2025/
Author
Jackey
Published at
2025-09-13
License
CC BY-NC-SA 4.0